mirror of
https://github.com/google/bumble.git
synced 2026-04-16 00:25:31 +00:00
608
apps/player/player.py
Normal file
608
apps/player/player.py
Normal file
@@ -0,0 +1,608 @@
|
|||||||
|
# Copyright 2024 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Imports
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
from __future__ import annotations
|
||||||
|
import asyncio
|
||||||
|
import asyncio.subprocess
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from bumble.a2dp import (
|
||||||
|
make_audio_source_service_sdp_records,
|
||||||
|
A2DP_SBC_CODEC_TYPE,
|
||||||
|
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
|
A2DP_NON_A2DP_CODEC_TYPE,
|
||||||
|
AacFrame,
|
||||||
|
AacParser,
|
||||||
|
AacPacketSource,
|
||||||
|
AacMediaCodecInformation,
|
||||||
|
SbcFrame,
|
||||||
|
SbcParser,
|
||||||
|
SbcPacketSource,
|
||||||
|
SbcMediaCodecInformation,
|
||||||
|
OpusPacket,
|
||||||
|
OpusParser,
|
||||||
|
OpusPacketSource,
|
||||||
|
OpusMediaCodecInformation,
|
||||||
|
)
|
||||||
|
from bumble.avrcp import Protocol as AvrcpProtocol
|
||||||
|
from bumble.avdtp import (
|
||||||
|
find_avdtp_service_with_connection,
|
||||||
|
AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
|
AVDTP_DELAY_REPORTING_SERVICE_CATEGORY,
|
||||||
|
MediaCodecCapabilities,
|
||||||
|
MediaPacketPump,
|
||||||
|
Protocol as AvdtpProtocol,
|
||||||
|
)
|
||||||
|
from bumble.colors import color
|
||||||
|
from bumble.core import (
|
||||||
|
AdvertisingData,
|
||||||
|
ConnectionError as BumbleConnectionError,
|
||||||
|
DeviceClass,
|
||||||
|
BT_BR_EDR_TRANSPORT,
|
||||||
|
)
|
||||||
|
from bumble.device import Connection, Device, DeviceConfiguration
|
||||||
|
from bumble.hci import Address, HCI_CONNECTION_ALREADY_EXISTS_ERROR, HCI_Constant
|
||||||
|
from bumble.pairing import PairingConfig
|
||||||
|
from bumble.transport import open_transport
|
||||||
|
from bumble.utils import AsyncRunner
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Logging
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def a2dp_source_sdp_records():
|
||||||
|
service_record_handle = 0x00010001
|
||||||
|
return {
|
||||||
|
service_record_handle: make_audio_source_service_sdp_records(
|
||||||
|
service_record_handle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def sbc_codec_capabilities(read_function) -> MediaCodecCapabilities:
|
||||||
|
sbc_parser = SbcParser(read_function)
|
||||||
|
sbc_frame: SbcFrame
|
||||||
|
async for sbc_frame in sbc_parser.frames:
|
||||||
|
# We only need the first frame
|
||||||
|
print(color(f"SBC format: {sbc_frame}", "cyan"))
|
||||||
|
break
|
||||||
|
|
||||||
|
channel_mode = [
|
||||||
|
SbcMediaCodecInformation.ChannelMode.MONO,
|
||||||
|
SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL,
|
||||||
|
SbcMediaCodecInformation.ChannelMode.STEREO,
|
||||||
|
SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
|
][sbc_frame.channel_mode]
|
||||||
|
block_length = {
|
||||||
|
4: SbcMediaCodecInformation.BlockLength.BL_4,
|
||||||
|
8: SbcMediaCodecInformation.BlockLength.BL_8,
|
||||||
|
12: SbcMediaCodecInformation.BlockLength.BL_12,
|
||||||
|
16: SbcMediaCodecInformation.BlockLength.BL_16,
|
||||||
|
}[sbc_frame.block_count]
|
||||||
|
subbands = {
|
||||||
|
4: SbcMediaCodecInformation.Subbands.S_4,
|
||||||
|
8: SbcMediaCodecInformation.Subbands.S_8,
|
||||||
|
}[sbc_frame.subband_count]
|
||||||
|
allocation_method = [
|
||||||
|
SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||||
|
SbcMediaCodecInformation.AllocationMethod.SNR,
|
||||||
|
][sbc_frame.allocation_method]
|
||||||
|
return MediaCodecCapabilities(
|
||||||
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.from_int(
|
||||||
|
sbc_frame.sampling_frequency
|
||||||
|
),
|
||||||
|
channel_mode=channel_mode,
|
||||||
|
block_length=block_length,
|
||||||
|
subbands=subbands,
|
||||||
|
allocation_method=allocation_method,
|
||||||
|
minimum_bitpool_value=2,
|
||||||
|
maximum_bitpool_value=40,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def aac_codec_capabilities(read_function) -> MediaCodecCapabilities:
|
||||||
|
aac_parser = AacParser(read_function)
|
||||||
|
aac_frame: AacFrame
|
||||||
|
async for aac_frame in aac_parser.frames:
|
||||||
|
# We only need the first frame
|
||||||
|
print(color(f"AAC format: {aac_frame}", "cyan"))
|
||||||
|
break
|
||||||
|
|
||||||
|
sampling_frequency = AacMediaCodecInformation.SamplingFrequency.from_int(
|
||||||
|
aac_frame.sampling_frequency
|
||||||
|
)
|
||||||
|
channels = (
|
||||||
|
AacMediaCodecInformation.Channels.MONO
|
||||||
|
if aac_frame.channel_configuration == 1
|
||||||
|
else AacMediaCodecInformation.Channels.STEREO
|
||||||
|
)
|
||||||
|
|
||||||
|
return MediaCodecCapabilities(
|
||||||
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
|
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
|
media_codec_information=AacMediaCodecInformation(
|
||||||
|
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
|
||||||
|
sampling_frequency=sampling_frequency,
|
||||||
|
channels=channels,
|
||||||
|
vbr=1,
|
||||||
|
bitrate=128000,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def opus_codec_capabilities(read_function) -> MediaCodecCapabilities:
|
||||||
|
opus_parser = OpusParser(read_function)
|
||||||
|
opus_packet: OpusPacket
|
||||||
|
async for opus_packet in opus_parser.packets:
|
||||||
|
# We only need the first packet
|
||||||
|
print(color(f"Opus format: {opus_packet}", "cyan"))
|
||||||
|
break
|
||||||
|
|
||||||
|
if opus_packet.channel_mode == OpusPacket.ChannelMode.MONO:
|
||||||
|
channel_mode = OpusMediaCodecInformation.ChannelMode.MONO
|
||||||
|
elif opus_packet.channel_mode == OpusPacket.ChannelMode.STEREO:
|
||||||
|
channel_mode = OpusMediaCodecInformation.ChannelMode.STEREO
|
||||||
|
else:
|
||||||
|
channel_mode = OpusMediaCodecInformation.ChannelMode.DUAL_MONO
|
||||||
|
|
||||||
|
if opus_packet.duration == 10:
|
||||||
|
frame_size = OpusMediaCodecInformation.FrameSize.FS_10MS
|
||||||
|
else:
|
||||||
|
frame_size = OpusMediaCodecInformation.FrameSize.FS_20MS
|
||||||
|
|
||||||
|
return MediaCodecCapabilities(
|
||||||
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
|
media_codec_type=A2DP_NON_A2DP_CODEC_TYPE,
|
||||||
|
media_codec_information=OpusMediaCodecInformation(
|
||||||
|
channel_mode=channel_mode,
|
||||||
|
sampling_frequency=OpusMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||||
|
frame_size=frame_size,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class Player:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
transport: str,
|
||||||
|
device_config: Optional[str],
|
||||||
|
authenticate: bool,
|
||||||
|
encrypt: bool,
|
||||||
|
) -> None:
|
||||||
|
self.transport = transport
|
||||||
|
self.device_config = device_config
|
||||||
|
self.authenticate = authenticate
|
||||||
|
self.encrypt = encrypt
|
||||||
|
self.avrcp_protocol: Optional[AvrcpProtocol] = None
|
||||||
|
self.done: Optional[asyncio.Event]
|
||||||
|
|
||||||
|
async def run(self, workload) -> None:
|
||||||
|
self.done = asyncio.Event()
|
||||||
|
try:
|
||||||
|
await self._run(workload)
|
||||||
|
except Exception as error:
|
||||||
|
print(color(f"!!! ERROR: {error}", "red"))
|
||||||
|
|
||||||
|
async def _run(self, workload) -> None:
|
||||||
|
async with await open_transport(self.transport) as (hci_source, hci_sink):
|
||||||
|
# Create a device
|
||||||
|
device_config = DeviceConfiguration()
|
||||||
|
if self.device_config:
|
||||||
|
device_config.load_from_file(self.device_config)
|
||||||
|
else:
|
||||||
|
device_config.name = "Bumble Player"
|
||||||
|
device_config.class_of_device = DeviceClass.pack_class_of_device(
|
||||||
|
DeviceClass.AUDIO_SERVICE_CLASS,
|
||||||
|
DeviceClass.AUDIO_VIDEO_MAJOR_DEVICE_CLASS,
|
||||||
|
DeviceClass.AUDIO_VIDEO_UNCATEGORIZED_MINOR_DEVICE_CLASS,
|
||||||
|
)
|
||||||
|
device_config.keystore = "JsonKeyStore"
|
||||||
|
|
||||||
|
device_config.classic_enabled = True
|
||||||
|
device_config.le_enabled = False
|
||||||
|
device_config.le_simultaneous_enabled = False
|
||||||
|
device_config.classic_sc_enabled = False
|
||||||
|
device_config.classic_smp_enabled = False
|
||||||
|
device = Device.from_config_with_hci(device_config, hci_source, hci_sink)
|
||||||
|
|
||||||
|
# Setup the SDP records to expose the SRC service
|
||||||
|
device.sdp_service_records = a2dp_source_sdp_records()
|
||||||
|
|
||||||
|
# Setup AVRCP
|
||||||
|
self.avrcp_protocol = AvrcpProtocol()
|
||||||
|
self.avrcp_protocol.listen(device)
|
||||||
|
|
||||||
|
# Don't require MITM when pairing.
|
||||||
|
device.pairing_config_factory = lambda connection: PairingConfig(mitm=False)
|
||||||
|
|
||||||
|
# Start the controller
|
||||||
|
await device.power_on()
|
||||||
|
|
||||||
|
# Print some of the config/properties
|
||||||
|
print(
|
||||||
|
"Player Bluetooth Address:",
|
||||||
|
color(
|
||||||
|
device.public_address.to_string(with_type_qualifier=False),
|
||||||
|
"yellow",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Listen for connections
|
||||||
|
device.on("connection", self.on_bluetooth_connection)
|
||||||
|
|
||||||
|
# Run the workload
|
||||||
|
try:
|
||||||
|
await workload(device)
|
||||||
|
except BumbleConnectionError as error:
|
||||||
|
if error.error_code == HCI_CONNECTION_ALREADY_EXISTS_ERROR:
|
||||||
|
print(color("Connection already established", "blue"))
|
||||||
|
else:
|
||||||
|
print(color(f"Failed to connect: {error}", "red"))
|
||||||
|
|
||||||
|
# Wait until it is time to exit
|
||||||
|
assert self.done is not None
|
||||||
|
await asyncio.wait(
|
||||||
|
[hci_source.terminated, asyncio.ensure_future(self.done.wait())],
|
||||||
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_bluetooth_connection(self, connection: Connection) -> None:
|
||||||
|
print(color(f"--- Connected: {connection}", "cyan"))
|
||||||
|
connection.on("disconnection", self.on_bluetooth_disconnection)
|
||||||
|
|
||||||
|
def on_bluetooth_disconnection(self, reason) -> None:
|
||||||
|
print(color(f"--- Disconnected: {HCI_Constant.error_name(reason)}", "cyan"))
|
||||||
|
self.set_done()
|
||||||
|
|
||||||
|
async def connect(self, device: Device, address: str) -> Connection:
|
||||||
|
print(color(f"Connecting to {address}...", "green"))
|
||||||
|
connection = await device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
||||||
|
|
||||||
|
# Request authentication
|
||||||
|
if self.authenticate:
|
||||||
|
print(color("*** Authenticating...", "blue"))
|
||||||
|
await connection.authenticate()
|
||||||
|
print(color("*** Authenticated", "blue"))
|
||||||
|
|
||||||
|
# Enable encryption
|
||||||
|
if self.encrypt:
|
||||||
|
print(color("*** Enabling encryption...", "blue"))
|
||||||
|
await connection.encrypt()
|
||||||
|
print(color("*** Encryption on", "blue"))
|
||||||
|
|
||||||
|
return connection
|
||||||
|
|
||||||
|
async def create_avdtp_protocol(self, connection: Connection) -> AvdtpProtocol:
|
||||||
|
# Look for an A2DP service
|
||||||
|
avdtp_version = await find_avdtp_service_with_connection(connection)
|
||||||
|
if not avdtp_version:
|
||||||
|
raise RuntimeError("no A2DP service found")
|
||||||
|
|
||||||
|
print(color(f"AVDTP Version: {avdtp_version}"))
|
||||||
|
|
||||||
|
# Create a client to interact with the remote device
|
||||||
|
return await AvdtpProtocol.connect(connection, avdtp_version)
|
||||||
|
|
||||||
|
async def stream_packets(
|
||||||
|
self,
|
||||||
|
protocol: AvdtpProtocol,
|
||||||
|
codec_type: int,
|
||||||
|
vendor_id: int,
|
||||||
|
codec_id: int,
|
||||||
|
packet_source: Union[SbcPacketSource, AacPacketSource, OpusPacketSource],
|
||||||
|
codec_capabilities: MediaCodecCapabilities,
|
||||||
|
):
|
||||||
|
# Discover all endpoints on the remote device
|
||||||
|
endpoints = await protocol.discover_remote_endpoints()
|
||||||
|
for endpoint in endpoints:
|
||||||
|
print('@@@', endpoint)
|
||||||
|
|
||||||
|
# Select a sink
|
||||||
|
sink = protocol.find_remote_sink_by_codec(
|
||||||
|
AVDTP_AUDIO_MEDIA_TYPE, codec_type, vendor_id, codec_id
|
||||||
|
)
|
||||||
|
if sink is None:
|
||||||
|
print(color('!!! no compatible sink found', 'red'))
|
||||||
|
return
|
||||||
|
print(f'### Selected sink: {sink.seid}')
|
||||||
|
|
||||||
|
# Check if the sink supports delay reporting
|
||||||
|
delay_reporting = False
|
||||||
|
for capability in sink.capabilities:
|
||||||
|
if capability.service_category == AVDTP_DELAY_REPORTING_SERVICE_CATEGORY:
|
||||||
|
delay_reporting = True
|
||||||
|
break
|
||||||
|
|
||||||
|
def on_delay_report(delay: int):
|
||||||
|
print(color(f"*** DELAY REPORT: {delay}", "blue"))
|
||||||
|
|
||||||
|
# Adjust the codec capabilities for certain codecs
|
||||||
|
for capability in sink.capabilities:
|
||||||
|
if isinstance(capability, MediaCodecCapabilities):
|
||||||
|
if isinstance(
|
||||||
|
codec_capabilities.media_codec_information, SbcMediaCodecInformation
|
||||||
|
) and isinstance(
|
||||||
|
capability.media_codec_information, SbcMediaCodecInformation
|
||||||
|
):
|
||||||
|
codec_capabilities.media_codec_information.minimum_bitpool_value = (
|
||||||
|
capability.media_codec_information.minimum_bitpool_value
|
||||||
|
)
|
||||||
|
codec_capabilities.media_codec_information.maximum_bitpool_value = (
|
||||||
|
capability.media_codec_information.maximum_bitpool_value
|
||||||
|
)
|
||||||
|
print(color("Source media codec:", "green"), codec_capabilities)
|
||||||
|
|
||||||
|
# Stream the packets
|
||||||
|
packet_pump = MediaPacketPump(packet_source.packets)
|
||||||
|
source = protocol.add_source(codec_capabilities, packet_pump, delay_reporting)
|
||||||
|
source.on("delay_report", on_delay_report)
|
||||||
|
stream = await protocol.create_stream(source, sink)
|
||||||
|
await stream.start()
|
||||||
|
|
||||||
|
await packet_pump.wait_for_completion()
|
||||||
|
|
||||||
|
async def discover(self, device: Device) -> None:
|
||||||
|
@device.listens_to("inquiry_result")
|
||||||
|
def on_inquiry_result(
|
||||||
|
address: Address, class_of_device: int, data: AdvertisingData, rssi: int
|
||||||
|
) -> None:
|
||||||
|
(
|
||||||
|
service_classes,
|
||||||
|
major_device_class,
|
||||||
|
minor_device_class,
|
||||||
|
) = DeviceClass.split_class_of_device(class_of_device)
|
||||||
|
separator = "\n "
|
||||||
|
print(f">>> {color(address.to_string(False), 'yellow')}:")
|
||||||
|
print(f" Device Class (raw): {class_of_device:06X}")
|
||||||
|
major_class_name = DeviceClass.major_device_class_name(major_device_class)
|
||||||
|
print(" Device Major Class: " f"{major_class_name}")
|
||||||
|
minor_class_name = DeviceClass.minor_device_class_name(
|
||||||
|
major_device_class, minor_device_class
|
||||||
|
)
|
||||||
|
print(" Device Minor Class: " f"{minor_class_name}")
|
||||||
|
print(
|
||||||
|
" Device Services: "
|
||||||
|
f"{', '.join(DeviceClass.service_class_labels(service_classes))}"
|
||||||
|
)
|
||||||
|
print(f" RSSI: {rssi}")
|
||||||
|
if data.ad_structures:
|
||||||
|
print(f" {data.to_string(separator)}")
|
||||||
|
|
||||||
|
await device.start_discovery()
|
||||||
|
|
||||||
|
async def pair(self, device: Device, address: str) -> None:
|
||||||
|
print(color(f"Connecting to {address}...", "green"))
|
||||||
|
connection = await device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
||||||
|
|
||||||
|
print(color("Pairing...", "magenta"))
|
||||||
|
await connection.authenticate()
|
||||||
|
print(color("Pairing completed", "magenta"))
|
||||||
|
self.set_done()
|
||||||
|
|
||||||
|
async def inquire(self, device: Device, address: str) -> None:
|
||||||
|
connection = await self.connect(device, address)
|
||||||
|
avdtp_protocol = await self.create_avdtp_protocol(connection)
|
||||||
|
|
||||||
|
# Discover the remote endpoints
|
||||||
|
endpoints = await avdtp_protocol.discover_remote_endpoints()
|
||||||
|
print(f'@@@ Found {len(list(endpoints))} endpoints')
|
||||||
|
for endpoint in endpoints:
|
||||||
|
print('@@@', endpoint)
|
||||||
|
|
||||||
|
self.set_done()
|
||||||
|
|
||||||
|
async def play(
|
||||||
|
self,
|
||||||
|
device: Device,
|
||||||
|
address: Optional[str],
|
||||||
|
audio_format: str,
|
||||||
|
audio_file: str,
|
||||||
|
) -> None:
|
||||||
|
if audio_format == "auto":
|
||||||
|
if audio_file.endswith(".sbc"):
|
||||||
|
audio_format = "sbc"
|
||||||
|
elif audio_file.endswith(".aac") or audio_file.endswith(".adts"):
|
||||||
|
audio_format = "aac"
|
||||||
|
elif audio_file.endswith(".ogg"):
|
||||||
|
audio_format = "opus"
|
||||||
|
else:
|
||||||
|
raise ValueError("Unable to determine audio format from file extension")
|
||||||
|
|
||||||
|
device.on(
|
||||||
|
"connection",
|
||||||
|
lambda connection: AsyncRunner.spawn(on_connection(connection)),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_connection(connection: Connection):
|
||||||
|
avdtp_protocol = await self.create_avdtp_protocol(connection)
|
||||||
|
|
||||||
|
with open(audio_file, 'rb') as input_file:
|
||||||
|
# NOTE: this should be using asyncio file reading, but blocking reads
|
||||||
|
# are good enough for this command line app.
|
||||||
|
async def read_audio_data(byte_count):
|
||||||
|
return input_file.read(byte_count)
|
||||||
|
|
||||||
|
# Obtain the codec capabilities from the stream
|
||||||
|
packet_source: Union[SbcPacketSource, AacPacketSource, OpusPacketSource]
|
||||||
|
vendor_id = 0
|
||||||
|
codec_id = 0
|
||||||
|
if audio_format == "sbc":
|
||||||
|
codec_type = A2DP_SBC_CODEC_TYPE
|
||||||
|
codec_capabilities = await sbc_codec_capabilities(read_audio_data)
|
||||||
|
packet_source = SbcPacketSource(
|
||||||
|
read_audio_data,
|
||||||
|
avdtp_protocol.l2cap_channel.peer_mtu,
|
||||||
|
)
|
||||||
|
elif audio_format == "aac":
|
||||||
|
codec_type = A2DP_MPEG_2_4_AAC_CODEC_TYPE
|
||||||
|
codec_capabilities = await aac_codec_capabilities(read_audio_data)
|
||||||
|
packet_source = AacPacketSource(
|
||||||
|
read_audio_data,
|
||||||
|
avdtp_protocol.l2cap_channel.peer_mtu,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
codec_type = A2DP_NON_A2DP_CODEC_TYPE
|
||||||
|
vendor_id = OpusMediaCodecInformation.VENDOR_ID
|
||||||
|
codec_id = OpusMediaCodecInformation.CODEC_ID
|
||||||
|
codec_capabilities = await opus_codec_capabilities(read_audio_data)
|
||||||
|
packet_source = OpusPacketSource(
|
||||||
|
read_audio_data,
|
||||||
|
avdtp_protocol.l2cap_channel.peer_mtu,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Rewind to the start
|
||||||
|
input_file.seek(0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.stream_packets(
|
||||||
|
avdtp_protocol,
|
||||||
|
codec_type,
|
||||||
|
vendor_id,
|
||||||
|
codec_id,
|
||||||
|
packet_source,
|
||||||
|
codec_capabilities,
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
print(color(f"!!! Error while streaming: {error}", "red"))
|
||||||
|
|
||||||
|
self.set_done()
|
||||||
|
|
||||||
|
if address:
|
||||||
|
await self.connect(device, address)
|
||||||
|
else:
|
||||||
|
print(color("Waiting for an incoming connection...", "magenta"))
|
||||||
|
|
||||||
|
def set_done(self) -> None:
|
||||||
|
if self.done:
|
||||||
|
self.done.set()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def create_player(context) -> Player:
|
||||||
|
return Player(
|
||||||
|
transport=context.obj["hci_transport"],
|
||||||
|
device_config=context.obj["device_config"],
|
||||||
|
authenticate=context.obj["authenticate"],
|
||||||
|
encrypt=context.obj["encrypt"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@click.group()
|
||||||
|
@click.pass_context
|
||||||
|
@click.option("--hci-transport", metavar="TRANSPORT", required=True)
|
||||||
|
@click.option("--device-config", metavar="FILENAME", help="Device configuration file")
|
||||||
|
@click.option(
|
||||||
|
"--authenticate",
|
||||||
|
is_flag=True,
|
||||||
|
help="Request authentication when connecting",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--encrypt", is_flag=True, help="Request encryption when connecting", default=True
|
||||||
|
)
|
||||||
|
def player_cli(ctx, hci_transport, device_config, authenticate, encrypt):
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj["hci_transport"] = hci_transport
|
||||||
|
ctx.obj["device_config"] = device_config
|
||||||
|
ctx.obj["authenticate"] = authenticate
|
||||||
|
ctx.obj["encrypt"] = encrypt
|
||||||
|
|
||||||
|
|
||||||
|
@player_cli.command("discover")
|
||||||
|
@click.pass_context
|
||||||
|
def discover(context):
|
||||||
|
"""Discover speakers or headphones"""
|
||||||
|
player = create_player(context)
|
||||||
|
asyncio.run(player.run(player.discover))
|
||||||
|
|
||||||
|
|
||||||
|
@player_cli.command("inquire")
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument(
|
||||||
|
"address",
|
||||||
|
metavar="ADDRESS",
|
||||||
|
)
|
||||||
|
def inquire(context, address):
|
||||||
|
"""Connect to a speaker or headphone and inquire about their capabilities"""
|
||||||
|
player = create_player(context)
|
||||||
|
asyncio.run(player.run(lambda device: player.inquire(device, address)))
|
||||||
|
|
||||||
|
|
||||||
|
@player_cli.command("pair")
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument(
|
||||||
|
"address",
|
||||||
|
metavar="ADDRESS",
|
||||||
|
)
|
||||||
|
def pair(context, address):
|
||||||
|
"""Pair with a speaker or headphone"""
|
||||||
|
player = create_player(context)
|
||||||
|
asyncio.run(player.run(lambda device: player.pair(device, address)))
|
||||||
|
|
||||||
|
|
||||||
|
@player_cli.command("play")
|
||||||
|
@click.pass_context
|
||||||
|
@click.option(
|
||||||
|
"--connect",
|
||||||
|
"address",
|
||||||
|
metavar="ADDRESS",
|
||||||
|
help="Address or name to connect to",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-f",
|
||||||
|
"--audio-format",
|
||||||
|
type=click.Choice(["auto", "sbc", "aac", "opus"]),
|
||||||
|
help="Audio file format (use 'auto' to infer the format from the file extension)",
|
||||||
|
default="auto",
|
||||||
|
)
|
||||||
|
@click.argument("audio_file")
|
||||||
|
def play(context, address, audio_format, audio_file):
|
||||||
|
"""Play and audio file"""
|
||||||
|
player = create_player(context)
|
||||||
|
asyncio.run(
|
||||||
|
player.run(
|
||||||
|
lambda device: player.play(device, address, audio_format, audio_file)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def main():
|
||||||
|
logging.basicConfig(level=os.environ.get("BUMBLE_LOGLEVEL", "WARNING").upper())
|
||||||
|
player_cli()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main() # pylint: disable=no-value-for-parameter
|
||||||
@@ -44,25 +44,18 @@ from bumble.avdtp import (
|
|||||||
AVDTP_AUDIO_MEDIA_TYPE,
|
AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
Listener,
|
Listener,
|
||||||
MediaCodecCapabilities,
|
MediaCodecCapabilities,
|
||||||
MediaPacket,
|
|
||||||
Protocol,
|
Protocol,
|
||||||
)
|
)
|
||||||
from bumble.a2dp import (
|
from bumble.a2dp import (
|
||||||
MPEG_2_AAC_LC_OBJECT_TYPE,
|
|
||||||
make_audio_sink_service_sdp_records,
|
make_audio_sink_service_sdp_records,
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
SBC_MONO_CHANNEL_MODE,
|
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
|
||||||
SbcMediaCodecInformation,
|
SbcMediaCodecInformation,
|
||||||
AacMediaCodecInformation,
|
AacMediaCodecInformation,
|
||||||
)
|
)
|
||||||
from bumble.utils import AsyncRunner
|
from bumble.utils import AsyncRunner
|
||||||
from bumble.codecs import AacAudioRtpPacket
|
from bumble.codecs import AacAudioRtpPacket
|
||||||
|
from bumble.rtp import MediaPacket
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -93,7 +86,7 @@ class AudioExtractor:
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AacAudioExtractor:
|
class AacAudioExtractor:
|
||||||
def extract_audio(self, packet: MediaPacket) -> bytes:
|
def extract_audio(self, packet: MediaPacket) -> bytes:
|
||||||
return AacAudioRtpPacket(packet.payload).to_adts()
|
return AacAudioRtpPacket.from_bytes(packet.payload).to_adts()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -451,10 +444,12 @@ class Speaker:
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
media_codec_information=AacMediaCodecInformation.from_lists(
|
media_codec_information=AacMediaCodecInformation(
|
||||||
object_types=[MPEG_2_AAC_LC_OBJECT_TYPE],
|
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
|
||||||
sampling_frequencies=[48000, 44100],
|
sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channels=[1, 2],
|
| AacMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||||
|
channels=AacMediaCodecInformation.Channels.MONO
|
||||||
|
| AacMediaCodecInformation.Channels.STEREO,
|
||||||
vbr=1,
|
vbr=1,
|
||||||
bitrate=256000,
|
bitrate=256000,
|
||||||
),
|
),
|
||||||
@@ -464,20 +459,23 @@ class Speaker:
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channel_modes=[
|
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
SBC_MONO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
],
|
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
block_lengths=[4, 8, 12, 16],
|
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
subbands=[4, 8],
|
block_length=SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
allocation_methods=[
|
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_16,
|
||||||
],
|
subbands=SbcMediaCodecInformation.Subbands.S_4
|
||||||
|
| SbcMediaCodecInformation.Subbands.S_8,
|
||||||
|
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||||
|
| SbcMediaCodecInformation.AllocationMethod.SNR,
|
||||||
minimum_bitpool_value=2,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
|
|||||||
708
bumble/a2dp.py
708
bumble/a2dp.py
@@ -17,12 +17,16 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses
|
|
||||||
import struct
|
|
||||||
import logging
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from typing import List, Callable, Awaitable
|
import dataclasses
|
||||||
|
import enum
|
||||||
|
import logging
|
||||||
|
import struct
|
||||||
|
from typing import Awaitable, Callable
|
||||||
|
from typing_extensions import ClassVar, Self
|
||||||
|
|
||||||
|
|
||||||
|
from .codecs import AacAudioRtpPacket
|
||||||
from .company_ids import COMPANY_IDENTIFIERS
|
from .company_ids import COMPANY_IDENTIFIERS
|
||||||
from .sdp import (
|
from .sdp import (
|
||||||
DataElement,
|
DataElement,
|
||||||
@@ -42,6 +46,7 @@ from .core import (
|
|||||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||||
name_or_number,
|
name_or_number,
|
||||||
)
|
)
|
||||||
|
from .rtp import MediaPacket
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -103,6 +108,8 @@ SBC_ALLOCATION_METHOD_NAMES = {
|
|||||||
SBC_LOUDNESS_ALLOCATION_METHOD: 'SBC_LOUDNESS_ALLOCATION_METHOD'
|
SBC_LOUDNESS_ALLOCATION_METHOD: 'SBC_LOUDNESS_ALLOCATION_METHOD'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SBC_MAX_FRAMES_IN_RTP_PAYLOAD = 15
|
||||||
|
|
||||||
MPEG_2_4_AAC_SAMPLING_FREQUENCIES = [
|
MPEG_2_4_AAC_SAMPLING_FREQUENCIES = [
|
||||||
8000,
|
8000,
|
||||||
11025,
|
11025,
|
||||||
@@ -130,6 +137,9 @@ MPEG_2_4_OBJECT_TYPE_NAMES = {
|
|||||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 'MPEG_4_AAC_SCALABLE_OBJECT_TYPE'
|
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 'MPEG_4_AAC_SCALABLE_OBJECT_TYPE'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
OPUS_MAX_FRAMES_IN_RTP_PAYLOAD = 15
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
@@ -257,38 +267,61 @@ class SbcMediaCodecInformation:
|
|||||||
A2DP spec - 4.3.2 Codec Specific Information Elements
|
A2DP spec - 4.3.2 Codec Specific Information Elements
|
||||||
'''
|
'''
|
||||||
|
|
||||||
sampling_frequency: int
|
sampling_frequency: SamplingFrequency
|
||||||
channel_mode: int
|
channel_mode: ChannelMode
|
||||||
block_length: int
|
block_length: BlockLength
|
||||||
subbands: int
|
subbands: Subbands
|
||||||
allocation_method: int
|
allocation_method: AllocationMethod
|
||||||
minimum_bitpool_value: int
|
minimum_bitpool_value: int
|
||||||
maximum_bitpool_value: int
|
maximum_bitpool_value: int
|
||||||
|
|
||||||
SAMPLING_FREQUENCY_BITS = {16000: 1 << 3, 32000: 1 << 2, 44100: 1 << 1, 48000: 1}
|
class SamplingFrequency(enum.IntFlag):
|
||||||
CHANNEL_MODE_BITS = {
|
SF_16000 = 1 << 3
|
||||||
SBC_MONO_CHANNEL_MODE: 1 << 3,
|
SF_32000 = 1 << 2
|
||||||
SBC_DUAL_CHANNEL_MODE: 1 << 2,
|
SF_44100 = 1 << 1
|
||||||
SBC_STEREO_CHANNEL_MODE: 1 << 1,
|
SF_48000 = 1 << 0
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE: 1,
|
|
||||||
}
|
|
||||||
BLOCK_LENGTH_BITS = {4: 1 << 3, 8: 1 << 2, 12: 1 << 1, 16: 1}
|
|
||||||
SUBBANDS_BITS = {4: 1 << 1, 8: 1}
|
|
||||||
ALLOCATION_METHOD_BITS = {
|
|
||||||
SBC_SNR_ALLOCATION_METHOD: 1 << 1,
|
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_bytes(data: bytes) -> SbcMediaCodecInformation:
|
def from_int(cls, sampling_frequency: int) -> Self:
|
||||||
sampling_frequency = (data[0] >> 4) & 0x0F
|
sampling_frequencies = [
|
||||||
channel_mode = (data[0] >> 0) & 0x0F
|
16000,
|
||||||
block_length = (data[1] >> 4) & 0x0F
|
32000,
|
||||||
subbands = (data[1] >> 2) & 0x03
|
44100,
|
||||||
allocation_method = (data[1] >> 0) & 0x03
|
48000,
|
||||||
|
]
|
||||||
|
index = sampling_frequencies.index(sampling_frequency)
|
||||||
|
return cls(1 << (len(sampling_frequencies) - index - 1))
|
||||||
|
|
||||||
|
class ChannelMode(enum.IntFlag):
|
||||||
|
MONO = 1 << 3
|
||||||
|
DUAL_CHANNEL = 1 << 2
|
||||||
|
STEREO = 1 << 1
|
||||||
|
JOINT_STEREO = 1 << 0
|
||||||
|
|
||||||
|
class BlockLength(enum.IntFlag):
|
||||||
|
BL_4 = 1 << 3
|
||||||
|
BL_8 = 1 << 2
|
||||||
|
BL_12 = 1 << 1
|
||||||
|
BL_16 = 1 << 0
|
||||||
|
|
||||||
|
class Subbands(enum.IntFlag):
|
||||||
|
S_4 = 1 << 1
|
||||||
|
S_8 = 1 << 0
|
||||||
|
|
||||||
|
class AllocationMethod(enum.IntFlag):
|
||||||
|
SNR = 1 << 1
|
||||||
|
LOUDNESS = 1 << 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes) -> Self:
|
||||||
|
sampling_frequency = cls.SamplingFrequency((data[0] >> 4) & 0x0F)
|
||||||
|
channel_mode = cls.ChannelMode((data[0] >> 0) & 0x0F)
|
||||||
|
block_length = cls.BlockLength((data[1] >> 4) & 0x0F)
|
||||||
|
subbands = cls.Subbands((data[1] >> 2) & 0x03)
|
||||||
|
allocation_method = cls.AllocationMethod((data[1] >> 0) & 0x03)
|
||||||
minimum_bitpool_value = (data[2] >> 0) & 0xFF
|
minimum_bitpool_value = (data[2] >> 0) & 0xFF
|
||||||
maximum_bitpool_value = (data[3] >> 0) & 0xFF
|
maximum_bitpool_value = (data[3] >> 0) & 0xFF
|
||||||
return SbcMediaCodecInformation(
|
return cls(
|
||||||
sampling_frequency,
|
sampling_frequency,
|
||||||
channel_mode,
|
channel_mode,
|
||||||
block_length,
|
block_length,
|
||||||
@@ -298,52 +331,6 @@ class SbcMediaCodecInformation:
|
|||||||
maximum_bitpool_value,
|
maximum_bitpool_value,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_discrete_values(
|
|
||||||
cls,
|
|
||||||
sampling_frequency: int,
|
|
||||||
channel_mode: int,
|
|
||||||
block_length: int,
|
|
||||||
subbands: int,
|
|
||||||
allocation_method: int,
|
|
||||||
minimum_bitpool_value: int,
|
|
||||||
maximum_bitpool_value: int,
|
|
||||||
) -> SbcMediaCodecInformation:
|
|
||||||
return SbcMediaCodecInformation(
|
|
||||||
sampling_frequency=cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
|
||||||
channel_mode=cls.CHANNEL_MODE_BITS[channel_mode],
|
|
||||||
block_length=cls.BLOCK_LENGTH_BITS[block_length],
|
|
||||||
subbands=cls.SUBBANDS_BITS[subbands],
|
|
||||||
allocation_method=cls.ALLOCATION_METHOD_BITS[allocation_method],
|
|
||||||
minimum_bitpool_value=minimum_bitpool_value,
|
|
||||||
maximum_bitpool_value=maximum_bitpool_value,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_lists(
|
|
||||||
cls,
|
|
||||||
sampling_frequencies: List[int],
|
|
||||||
channel_modes: List[int],
|
|
||||||
block_lengths: List[int],
|
|
||||||
subbands: List[int],
|
|
||||||
allocation_methods: List[int],
|
|
||||||
minimum_bitpool_value: int,
|
|
||||||
maximum_bitpool_value: int,
|
|
||||||
) -> SbcMediaCodecInformation:
|
|
||||||
return SbcMediaCodecInformation(
|
|
||||||
sampling_frequency=sum(
|
|
||||||
cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies
|
|
||||||
),
|
|
||||||
channel_mode=sum(cls.CHANNEL_MODE_BITS[x] for x in channel_modes),
|
|
||||||
block_length=sum(cls.BLOCK_LENGTH_BITS[x] for x in block_lengths),
|
|
||||||
subbands=sum(cls.SUBBANDS_BITS[x] for x in subbands),
|
|
||||||
allocation_method=sum(
|
|
||||||
cls.ALLOCATION_METHOD_BITS[x] for x in allocation_methods
|
|
||||||
),
|
|
||||||
minimum_bitpool_value=minimum_bitpool_value,
|
|
||||||
maximum_bitpool_value=maximum_bitpool_value,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __bytes__(self) -> bytes:
|
def __bytes__(self) -> bytes:
|
||||||
return bytes(
|
return bytes(
|
||||||
[
|
[
|
||||||
@@ -356,23 +343,6 @@ class SbcMediaCodecInformation:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO']
|
|
||||||
allocation_methods = ['SNR', 'Loudness']
|
|
||||||
return '\n'.join(
|
|
||||||
# pylint: disable=line-too-long
|
|
||||||
[
|
|
||||||
'SbcMediaCodecInformation(',
|
|
||||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, SBC_SAMPLING_FREQUENCIES)])}',
|
|
||||||
f' channel_mode: {",".join([str(x) for x in flags_to_list(self.channel_mode, channel_modes)])}',
|
|
||||||
f' block_length: {",".join([str(x) for x in flags_to_list(self.block_length, SBC_BLOCK_LENGTHS)])}',
|
|
||||||
f' subbands: {",".join([str(x) for x in flags_to_list(self.subbands, SBC_SUBBANDS)])}',
|
|
||||||
f' allocation_method: {",".join([str(x) for x in flags_to_list(self.allocation_method, allocation_methods)])}',
|
|
||||||
f' minimum_bitpool_value: {self.minimum_bitpool_value}',
|
|
||||||
f' maximum_bitpool_value: {self.maximum_bitpool_value}' ')',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
@@ -381,83 +351,66 @@ class AacMediaCodecInformation:
|
|||||||
A2DP spec - 4.5.2 Codec Specific Information Elements
|
A2DP spec - 4.5.2 Codec Specific Information Elements
|
||||||
'''
|
'''
|
||||||
|
|
||||||
object_type: int
|
object_type: ObjectType
|
||||||
sampling_frequency: int
|
sampling_frequency: SamplingFrequency
|
||||||
channels: int
|
channels: Channels
|
||||||
rfa: int
|
|
||||||
vbr: int
|
vbr: int
|
||||||
bitrate: int
|
bitrate: int
|
||||||
|
|
||||||
OBJECT_TYPE_BITS = {
|
class ObjectType(enum.IntFlag):
|
||||||
MPEG_2_AAC_LC_OBJECT_TYPE: 1 << 7,
|
MPEG_2_AAC_LC = 1 << 7
|
||||||
MPEG_4_AAC_LC_OBJECT_TYPE: 1 << 6,
|
MPEG_4_AAC_LC = 1 << 6
|
||||||
MPEG_4_AAC_LTP_OBJECT_TYPE: 1 << 5,
|
MPEG_4_AAC_LTP = 1 << 5
|
||||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 1 << 4,
|
MPEG_4_AAC_SCALABLE = 1 << 4
|
||||||
}
|
|
||||||
SAMPLING_FREQUENCY_BITS = {
|
|
||||||
8000: 1 << 11,
|
|
||||||
11025: 1 << 10,
|
|
||||||
12000: 1 << 9,
|
|
||||||
16000: 1 << 8,
|
|
||||||
22050: 1 << 7,
|
|
||||||
24000: 1 << 6,
|
|
||||||
32000: 1 << 5,
|
|
||||||
44100: 1 << 4,
|
|
||||||
48000: 1 << 3,
|
|
||||||
64000: 1 << 2,
|
|
||||||
88200: 1 << 1,
|
|
||||||
96000: 1,
|
|
||||||
}
|
|
||||||
CHANNELS_BITS = {1: 1 << 1, 2: 1}
|
|
||||||
|
|
||||||
@staticmethod
|
class SamplingFrequency(enum.IntFlag):
|
||||||
def from_bytes(data: bytes) -> AacMediaCodecInformation:
|
SF_8000 = 1 << 11
|
||||||
object_type = data[0]
|
SF_11025 = 1 << 10
|
||||||
sampling_frequency = (data[1] << 4) | ((data[2] >> 4) & 0x0F)
|
SF_12000 = 1 << 9
|
||||||
channels = (data[2] >> 2) & 0x03
|
SF_16000 = 1 << 8
|
||||||
rfa = 0
|
SF_22050 = 1 << 7
|
||||||
|
SF_24000 = 1 << 6
|
||||||
|
SF_32000 = 1 << 5
|
||||||
|
SF_44100 = 1 << 4
|
||||||
|
SF_48000 = 1 << 3
|
||||||
|
SF_64000 = 1 << 2
|
||||||
|
SF_88200 = 1 << 1
|
||||||
|
SF_96000 = 1 << 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_int(cls, sampling_frequency: int) -> Self:
|
||||||
|
sampling_frequencies = [
|
||||||
|
8000,
|
||||||
|
11025,
|
||||||
|
12000,
|
||||||
|
16000,
|
||||||
|
22050,
|
||||||
|
24000,
|
||||||
|
32000,
|
||||||
|
44100,
|
||||||
|
48000,
|
||||||
|
64000,
|
||||||
|
88200,
|
||||||
|
96000,
|
||||||
|
]
|
||||||
|
index = sampling_frequencies.index(sampling_frequency)
|
||||||
|
return cls(1 << (len(sampling_frequencies) - index - 1))
|
||||||
|
|
||||||
|
class Channels(enum.IntFlag):
|
||||||
|
MONO = 1 << 1
|
||||||
|
STEREO = 1 << 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes) -> AacMediaCodecInformation:
|
||||||
|
object_type = cls.ObjectType(data[0])
|
||||||
|
sampling_frequency = cls.SamplingFrequency(
|
||||||
|
(data[1] << 4) | ((data[2] >> 4) & 0x0F)
|
||||||
|
)
|
||||||
|
channels = cls.Channels((data[2] >> 2) & 0x03)
|
||||||
vbr = (data[3] >> 7) & 0x01
|
vbr = (data[3] >> 7) & 0x01
|
||||||
bitrate = ((data[3] & 0x7F) << 16) | (data[4] << 8) | data[5]
|
bitrate = ((data[3] & 0x7F) << 16) | (data[4] << 8) | data[5]
|
||||||
return AacMediaCodecInformation(
|
return AacMediaCodecInformation(
|
||||||
object_type, sampling_frequency, channels, rfa, vbr, bitrate
|
object_type, sampling_frequency, channels, vbr, bitrate
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_discrete_values(
|
|
||||||
cls,
|
|
||||||
object_type: int,
|
|
||||||
sampling_frequency: int,
|
|
||||||
channels: int,
|
|
||||||
vbr: int,
|
|
||||||
bitrate: int,
|
|
||||||
) -> AacMediaCodecInformation:
|
|
||||||
return AacMediaCodecInformation(
|
|
||||||
object_type=cls.OBJECT_TYPE_BITS[object_type],
|
|
||||||
sampling_frequency=cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
|
||||||
channels=cls.CHANNELS_BITS[channels],
|
|
||||||
rfa=0,
|
|
||||||
vbr=vbr,
|
|
||||||
bitrate=bitrate,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_lists(
|
|
||||||
cls,
|
|
||||||
object_types: List[int],
|
|
||||||
sampling_frequencies: List[int],
|
|
||||||
channels: List[int],
|
|
||||||
vbr: int,
|
|
||||||
bitrate: int,
|
|
||||||
) -> AacMediaCodecInformation:
|
|
||||||
return AacMediaCodecInformation(
|
|
||||||
object_type=sum(cls.OBJECT_TYPE_BITS[x] for x in object_types),
|
|
||||||
sampling_frequency=sum(
|
|
||||||
cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies
|
|
||||||
),
|
|
||||||
channels=sum(cls.CHANNELS_BITS[x] for x in channels),
|
|
||||||
rfa=0,
|
|
||||||
vbr=vbr,
|
|
||||||
bitrate=bitrate,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __bytes__(self) -> bytes:
|
def __bytes__(self) -> bytes:
|
||||||
@@ -472,30 +425,6 @@ class AacMediaCodecInformation:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
object_types = [
|
|
||||||
'MPEG_2_AAC_LC',
|
|
||||||
'MPEG_4_AAC_LC',
|
|
||||||
'MPEG_4_AAC_LTP',
|
|
||||||
'MPEG_4_AAC_SCALABLE',
|
|
||||||
'[4]',
|
|
||||||
'[5]',
|
|
||||||
'[6]',
|
|
||||||
'[7]',
|
|
||||||
]
|
|
||||||
channels = [1, 2]
|
|
||||||
# pylint: disable=line-too-long
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
'AacMediaCodecInformation(',
|
|
||||||
f' object_type: {",".join([str(x) for x in flags_to_list(self.object_type, object_types)])}',
|
|
||||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, MPEG_2_4_AAC_SAMPLING_FREQUENCIES)])}',
|
|
||||||
f' channels: {",".join([str(x) for x in flags_to_list(self.channels, channels)])}',
|
|
||||||
f' vbr: {self.vbr}',
|
|
||||||
f' bitrate: {self.bitrate}' ')',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -514,7 +443,7 @@ class VendorSpecificMediaCodecInformation:
|
|||||||
return VendorSpecificMediaCodecInformation(vendor_id, codec_id, data[6:])
|
return VendorSpecificMediaCodecInformation(vendor_id, codec_id, data[6:])
|
||||||
|
|
||||||
def __bytes__(self) -> bytes:
|
def __bytes__(self) -> bytes:
|
||||||
return struct.pack('<IH', self.vendor_id, self.codec_id, self.value)
|
return struct.pack('<IH', self.vendor_id, self.codec_id) + self.value
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
@@ -528,13 +457,69 @@ class VendorSpecificMediaCodecInformation:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class OpusMediaCodecInformation(VendorSpecificMediaCodecInformation):
|
||||||
|
vendor_id: int = dataclasses.field(init=False, repr=False)
|
||||||
|
codec_id: int = dataclasses.field(init=False, repr=False)
|
||||||
|
value: bytes = dataclasses.field(init=False, repr=False)
|
||||||
|
channel_mode: ChannelMode
|
||||||
|
frame_size: FrameSize
|
||||||
|
sampling_frequency: SamplingFrequency
|
||||||
|
|
||||||
|
class ChannelMode(enum.IntFlag):
|
||||||
|
MONO = 1 << 0
|
||||||
|
STEREO = 1 << 1
|
||||||
|
DUAL_MONO = 1 << 2
|
||||||
|
|
||||||
|
class FrameSize(enum.IntFlag):
|
||||||
|
FS_10MS = 1 << 0
|
||||||
|
FS_20MS = 1 << 1
|
||||||
|
|
||||||
|
class SamplingFrequency(enum.IntFlag):
|
||||||
|
SF_48000 = 1 << 0
|
||||||
|
|
||||||
|
VENDOR_ID: ClassVar[int] = 0x000000E0
|
||||||
|
CODEC_ID: ClassVar[int] = 0x0001
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self.vendor_id = self.VENDOR_ID
|
||||||
|
self.codec_id = self.CODEC_ID
|
||||||
|
self.value = bytes(
|
||||||
|
[
|
||||||
|
self.channel_mode
|
||||||
|
| (self.frame_size << 3)
|
||||||
|
| (self.sampling_frequency << 7)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes) -> Self:
|
||||||
|
"""Create a new instance from the `value` part of the data, not including
|
||||||
|
the vendor id and codec id"""
|
||||||
|
channel_mode = cls.ChannelMode(data[0] & 0x07)
|
||||||
|
frame_size = cls.FrameSize((data[0] >> 3) & 0x03)
|
||||||
|
sampling_frequency = cls.SamplingFrequency((data[0] >> 7) & 0x01)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
channel_mode,
|
||||||
|
frame_size,
|
||||||
|
sampling_frequency,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return repr(self)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class SbcFrame:
|
class SbcFrame:
|
||||||
sampling_frequency: int
|
sampling_frequency: int
|
||||||
block_count: int
|
block_count: int
|
||||||
channel_mode: int
|
channel_mode: int
|
||||||
|
allocation_method: int
|
||||||
subband_count: int
|
subband_count: int
|
||||||
|
bitpool: int
|
||||||
payload: bytes
|
payload: bytes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -553,8 +538,10 @@ class SbcFrame:
|
|||||||
return (
|
return (
|
||||||
f'SBC(sf={self.sampling_frequency},'
|
f'SBC(sf={self.sampling_frequency},'
|
||||||
f'cm={self.channel_mode},'
|
f'cm={self.channel_mode},'
|
||||||
|
f'am={self.allocation_method},'
|
||||||
f'br={self.bitrate},'
|
f'br={self.bitrate},'
|
||||||
f'sc={self.sample_count},'
|
f'sc={self.sample_count},'
|
||||||
|
f'bp={self.bitpool},'
|
||||||
f'size={len(self.payload)})'
|
f'size={len(self.payload)})'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -583,6 +570,7 @@ class SbcParser:
|
|||||||
blocks = 4 * (1 + ((header[1] >> 4) & 3))
|
blocks = 4 * (1 + ((header[1] >> 4) & 3))
|
||||||
channel_mode = (header[1] >> 2) & 3
|
channel_mode = (header[1] >> 2) & 3
|
||||||
channels = 1 if channel_mode == SBC_MONO_CHANNEL_MODE else 2
|
channels = 1 if channel_mode == SBC_MONO_CHANNEL_MODE else 2
|
||||||
|
allocation_method = (header[1] >> 1) & 1
|
||||||
subbands = 8 if ((header[1]) & 1) else 4
|
subbands = 8 if ((header[1]) & 1) else 4
|
||||||
bitpool = header[2]
|
bitpool = header[2]
|
||||||
|
|
||||||
@@ -602,7 +590,13 @@ class SbcParser:
|
|||||||
|
|
||||||
# Emit the next frame
|
# Emit the next frame
|
||||||
yield SbcFrame(
|
yield SbcFrame(
|
||||||
sampling_frequency, blocks, channel_mode, subbands, payload
|
sampling_frequency,
|
||||||
|
blocks,
|
||||||
|
channel_mode,
|
||||||
|
allocation_method,
|
||||||
|
subbands,
|
||||||
|
bitpool,
|
||||||
|
payload,
|
||||||
)
|
)
|
||||||
|
|
||||||
return generate_frames()
|
return generate_frames()
|
||||||
@@ -610,21 +604,15 @@ class SbcParser:
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class SbcPacketSource:
|
class SbcPacketSource:
|
||||||
def __init__(
|
def __init__(self, read: Callable[[int], Awaitable[bytes]], mtu: int) -> None:
|
||||||
self, read: Callable[[int], Awaitable[bytes]], mtu: int, codec_capabilities
|
|
||||||
) -> None:
|
|
||||||
self.read = read
|
self.read = read
|
||||||
self.mtu = mtu
|
self.mtu = mtu
|
||||||
self.codec_capabilities = codec_capabilities
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def packets(self):
|
def packets(self):
|
||||||
async def generate_packets():
|
async def generate_packets():
|
||||||
# pylint: disable=import-outside-toplevel
|
|
||||||
from .avdtp import MediaPacket # Import here to avoid a circular reference
|
|
||||||
|
|
||||||
sequence_number = 0
|
sequence_number = 0
|
||||||
timestamp = 0
|
sample_count = 0
|
||||||
frames = []
|
frames = []
|
||||||
frames_size = 0
|
frames_size = 0
|
||||||
max_rtp_payload = self.mtu - 12 - 1
|
max_rtp_payload = self.mtu - 12 - 1
|
||||||
@@ -632,29 +620,29 @@ class SbcPacketSource:
|
|||||||
# NOTE: this doesn't support frame fragments
|
# NOTE: this doesn't support frame fragments
|
||||||
sbc_parser = SbcParser(self.read)
|
sbc_parser = SbcParser(self.read)
|
||||||
async for frame in sbc_parser.frames:
|
async for frame in sbc_parser.frames:
|
||||||
print(frame)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
frames_size + len(frame.payload) > max_rtp_payload
|
frames_size + len(frame.payload) > max_rtp_payload
|
||||||
or len(frames) == 16
|
or len(frames) == SBC_MAX_FRAMES_IN_RTP_PAYLOAD
|
||||||
):
|
):
|
||||||
# Need to flush what has been accumulated so far
|
# Need to flush what has been accumulated so far
|
||||||
|
logger.debug(f"yielding {len(frames)} frames")
|
||||||
|
|
||||||
# Emit a packet
|
# Emit a packet
|
||||||
sbc_payload = bytes([len(frames)]) + b''.join(
|
sbc_payload = bytes([len(frames) & 0x0F]) + b''.join(
|
||||||
[frame.payload for frame in frames]
|
[frame.payload for frame in frames]
|
||||||
)
|
)
|
||||||
|
timestamp_seconds = sample_count / frame.sampling_frequency
|
||||||
|
timestamp = int(1000 * timestamp_seconds)
|
||||||
packet = MediaPacket(
|
packet = MediaPacket(
|
||||||
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, sbc_payload
|
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, sbc_payload
|
||||||
)
|
)
|
||||||
packet.timestamp_seconds = timestamp / frame.sampling_frequency
|
packet.timestamp_seconds = timestamp_seconds
|
||||||
yield packet
|
yield packet
|
||||||
|
|
||||||
# Prepare for next packets
|
# Prepare for next packets
|
||||||
sequence_number += 1
|
sequence_number += 1
|
||||||
sequence_number &= 0xFFFF
|
sequence_number &= 0xFFFF
|
||||||
timestamp += sum((frame.sample_count for frame in frames))
|
sample_count += sum((frame.sample_count for frame in frames))
|
||||||
timestamp &= 0xFFFFFFFF
|
|
||||||
frames = [frame]
|
frames = [frame]
|
||||||
frames_size = len(frame.payload)
|
frames_size = len(frame.payload)
|
||||||
else:
|
else:
|
||||||
@@ -663,3 +651,315 @@ class SbcPacketSource:
|
|||||||
frames_size += len(frame.payload)
|
frames_size += len(frame.payload)
|
||||||
|
|
||||||
return generate_packets()
|
return generate_packets()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class AacFrame:
|
||||||
|
class Profile(enum.IntEnum):
|
||||||
|
MAIN = 0
|
||||||
|
LC = 1
|
||||||
|
SSR = 2
|
||||||
|
LTP = 3
|
||||||
|
|
||||||
|
profile: Profile
|
||||||
|
sampling_frequency: int
|
||||||
|
channel_configuration: int
|
||||||
|
payload: bytes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sample_count(self) -> int:
|
||||||
|
return 1024
|
||||||
|
|
||||||
|
@property
|
||||||
|
def duration(self) -> float:
|
||||||
|
return self.sample_count / self.sampling_frequency
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return (
|
||||||
|
f'AAC(sf={self.sampling_frequency},'
|
||||||
|
f'ch={self.channel_configuration},'
|
||||||
|
f'size={len(self.payload)})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
ADTS_AAC_SAMPLING_FREQUENCIES = [
|
||||||
|
96000,
|
||||||
|
88200,
|
||||||
|
64000,
|
||||||
|
48000,
|
||||||
|
44100,
|
||||||
|
32000,
|
||||||
|
24000,
|
||||||
|
22050,
|
||||||
|
16000,
|
||||||
|
12000,
|
||||||
|
11025,
|
||||||
|
8000,
|
||||||
|
7350,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class AacParser:
|
||||||
|
"""Parser for AAC frames in an ADTS stream"""
|
||||||
|
|
||||||
|
def __init__(self, read: Callable[[int], Awaitable[bytes]]) -> None:
|
||||||
|
self.read = read
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frames(self) -> AsyncGenerator[AacFrame, None]:
|
||||||
|
async def generate_frames() -> AsyncGenerator[AacFrame, None]:
|
||||||
|
while True:
|
||||||
|
header = await self.read(7)
|
||||||
|
if not header:
|
||||||
|
return
|
||||||
|
|
||||||
|
sync_word = (header[0] << 4) | (header[1] >> 4)
|
||||||
|
if sync_word != 0b111111111111:
|
||||||
|
raise ValueError(f"invalid sync word ({sync_word:06x})")
|
||||||
|
layer = (header[1] >> 1) & 0b11
|
||||||
|
profile = AacFrame.Profile((header[2] >> 6) & 0b11)
|
||||||
|
sampling_frequency = ADTS_AAC_SAMPLING_FREQUENCIES[
|
||||||
|
(header[2] >> 2) & 0b1111
|
||||||
|
]
|
||||||
|
channel_configuration = ((header[2] & 0b1) << 2) | (header[3] >> 6)
|
||||||
|
frame_length = (
|
||||||
|
((header[3] & 0b11) << 11) | (header[4] << 3) | (header[5] >> 5)
|
||||||
|
)
|
||||||
|
|
||||||
|
if layer != 0:
|
||||||
|
raise ValueError("layer must be 0")
|
||||||
|
|
||||||
|
payload = await self.read(frame_length - 7)
|
||||||
|
if payload:
|
||||||
|
yield AacFrame(
|
||||||
|
profile, sampling_frequency, channel_configuration, payload
|
||||||
|
)
|
||||||
|
|
||||||
|
return generate_frames()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class AacPacketSource:
|
||||||
|
def __init__(self, read: Callable[[int], Awaitable[bytes]], mtu: int) -> None:
|
||||||
|
self.read = read
|
||||||
|
self.mtu = mtu
|
||||||
|
|
||||||
|
@property
|
||||||
|
def packets(self):
|
||||||
|
async def generate_packets():
|
||||||
|
sequence_number = 0
|
||||||
|
sample_count = 0
|
||||||
|
|
||||||
|
aac_parser = AacParser(self.read)
|
||||||
|
async for frame in aac_parser.frames:
|
||||||
|
logger.debug("yielding one AAC frame")
|
||||||
|
|
||||||
|
# Emit a packet
|
||||||
|
aac_payload = bytes(
|
||||||
|
AacAudioRtpPacket.for_simple_aac(
|
||||||
|
frame.sampling_frequency,
|
||||||
|
frame.channel_configuration,
|
||||||
|
frame.payload,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
timestamp_seconds = sample_count / frame.sampling_frequency
|
||||||
|
timestamp = int(1000 * timestamp_seconds)
|
||||||
|
packet = MediaPacket(
|
||||||
|
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, aac_payload
|
||||||
|
)
|
||||||
|
packet.timestamp_seconds = timestamp_seconds
|
||||||
|
yield packet
|
||||||
|
|
||||||
|
# Prepare for next packets
|
||||||
|
sequence_number += 1
|
||||||
|
sequence_number &= 0xFFFF
|
||||||
|
sample_count += frame.sample_count
|
||||||
|
|
||||||
|
return generate_packets()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class OpusPacket:
|
||||||
|
class ChannelMode(enum.IntEnum):
|
||||||
|
MONO = 0
|
||||||
|
STEREO = 1
|
||||||
|
DUAL_MONO = 2
|
||||||
|
|
||||||
|
channel_mode: ChannelMode
|
||||||
|
duration: int # Duration in ms.
|
||||||
|
sampling_frequency: int
|
||||||
|
payload: bytes
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return (
|
||||||
|
f'Opus(ch={self.channel_mode.name}, '
|
||||||
|
f'd={self.duration}ms, '
|
||||||
|
f'size={len(self.payload)})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class OpusParser:
|
||||||
|
"""
|
||||||
|
Parser for Opus packets in an Ogg stream
|
||||||
|
|
||||||
|
See RFC 3533
|
||||||
|
|
||||||
|
NOTE: this parser only supports bitstreams with a single logical stream.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CAPTURE_PATTERN = b'OggS'
|
||||||
|
|
||||||
|
class HeaderType(enum.IntFlag):
|
||||||
|
CONTINUED = 0x01
|
||||||
|
FIRST = 0x02
|
||||||
|
LAST = 0x04
|
||||||
|
|
||||||
|
def __init__(self, read: Callable[[int], Awaitable[bytes]]) -> None:
|
||||||
|
self.read = read
|
||||||
|
|
||||||
|
@property
|
||||||
|
def packets(self) -> AsyncGenerator[OpusPacket, None]:
|
||||||
|
async def generate_frames() -> AsyncGenerator[OpusPacket, None]:
|
||||||
|
packet = b''
|
||||||
|
packet_count = 0
|
||||||
|
expected_bitstream_serial_number = None
|
||||||
|
expected_page_sequence_number = 0
|
||||||
|
channel_mode = OpusPacket.ChannelMode.STEREO
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Parse the page header
|
||||||
|
header = await self.read(27)
|
||||||
|
if len(header) != 27:
|
||||||
|
logger.debug("end of stream")
|
||||||
|
break
|
||||||
|
|
||||||
|
capture_pattern = header[:4]
|
||||||
|
if capture_pattern != self.CAPTURE_PATTERN:
|
||||||
|
print(capture_pattern.hex())
|
||||||
|
raise ValueError("invalid capture pattern at start of page")
|
||||||
|
|
||||||
|
version = header[4]
|
||||||
|
if version != 0:
|
||||||
|
raise ValueError(f"version {version} not supported")
|
||||||
|
|
||||||
|
header_type = self.HeaderType(header[5])
|
||||||
|
(
|
||||||
|
granule_position,
|
||||||
|
bitstream_serial_number,
|
||||||
|
page_sequence_number,
|
||||||
|
crc_checksum,
|
||||||
|
page_segments,
|
||||||
|
) = struct.unpack_from("<QIIIB", header, 6)
|
||||||
|
segment_table = await self.read(page_segments)
|
||||||
|
|
||||||
|
if header_type & self.HeaderType.FIRST:
|
||||||
|
if expected_bitstream_serial_number is None:
|
||||||
|
# We will only accept pages for the first encountered stream
|
||||||
|
logger.debug("BOS")
|
||||||
|
expected_bitstream_serial_number = bitstream_serial_number
|
||||||
|
expected_page_sequence_number = page_sequence_number
|
||||||
|
|
||||||
|
if (
|
||||||
|
expected_bitstream_serial_number is None
|
||||||
|
or expected_bitstream_serial_number != bitstream_serial_number
|
||||||
|
):
|
||||||
|
logger.debug("skipping page (not the first logical bitstream)")
|
||||||
|
for lacing_value in segment_table:
|
||||||
|
if lacing_value:
|
||||||
|
await self.read(lacing_value)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if expected_page_sequence_number != page_sequence_number:
|
||||||
|
raise ValueError(
|
||||||
|
f"expected page sequence number {expected_page_sequence_number}"
|
||||||
|
f" but got {page_sequence_number}"
|
||||||
|
)
|
||||||
|
expected_page_sequence_number = page_sequence_number + 1
|
||||||
|
|
||||||
|
# Assemble the page
|
||||||
|
if not header_type & self.HeaderType.CONTINUED:
|
||||||
|
packet = b''
|
||||||
|
for lacing_value in segment_table:
|
||||||
|
if lacing_value:
|
||||||
|
packet += await self.read(lacing_value)
|
||||||
|
if lacing_value < 255:
|
||||||
|
# End of packet
|
||||||
|
packet_count += 1
|
||||||
|
|
||||||
|
if packet_count == 1:
|
||||||
|
# The first packet contains the identification header
|
||||||
|
logger.debug("first packet (header)")
|
||||||
|
if packet[:8] != b"OpusHead":
|
||||||
|
raise ValueError("first packet is not OpusHead")
|
||||||
|
packet_count = (
|
||||||
|
OpusPacket.ChannelMode.MONO
|
||||||
|
if packet[9] == 1
|
||||||
|
else OpusPacket.ChannelMode.STEREO
|
||||||
|
)
|
||||||
|
|
||||||
|
elif packet_count == 2:
|
||||||
|
# The second packet contains the comment header
|
||||||
|
logger.debug("second packet (tags)")
|
||||||
|
if packet[:8] != b"OpusTags":
|
||||||
|
logger.warning("second packet is not OpusTags")
|
||||||
|
else:
|
||||||
|
yield OpusPacket(channel_mode, 20, 48000, packet)
|
||||||
|
|
||||||
|
packet = b''
|
||||||
|
|
||||||
|
if header_type & self.HeaderType.LAST:
|
||||||
|
logger.debug("EOS")
|
||||||
|
|
||||||
|
return generate_frames()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class OpusPacketSource:
|
||||||
|
def __init__(self, read: Callable[[int], Awaitable[bytes]], mtu: int) -> None:
|
||||||
|
self.read = read
|
||||||
|
self.mtu = mtu
|
||||||
|
|
||||||
|
@property
|
||||||
|
def packets(self):
|
||||||
|
async def generate_packets():
|
||||||
|
sequence_number = 0
|
||||||
|
elapsed_ms = 0
|
||||||
|
|
||||||
|
opus_parser = OpusParser(self.read)
|
||||||
|
async for opus_packet in opus_parser.packets:
|
||||||
|
# We only support sending one Opus frame per RTP packet
|
||||||
|
# TODO: check the spec for the first byte value here
|
||||||
|
opus_payload = bytes([1]) + opus_packet.payload
|
||||||
|
elapsed_s = elapsed_ms / 1000
|
||||||
|
timestamp = int(elapsed_s * opus_packet.sampling_frequency)
|
||||||
|
rtp_packet = MediaPacket(
|
||||||
|
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, opus_payload
|
||||||
|
)
|
||||||
|
rtp_packet.timestamp_seconds = elapsed_s
|
||||||
|
yield rtp_packet
|
||||||
|
|
||||||
|
# Prepare for next packets
|
||||||
|
sequence_number += 1
|
||||||
|
sequence_number &= 0xFFFF
|
||||||
|
elapsed_ms += opus_packet.duration
|
||||||
|
|
||||||
|
return generate_packets()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# This map should be left at the end of the file so it can refer to the classes
|
||||||
|
# above
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
A2DP_VENDOR_MEDIA_CODEC_INFORMATION_CLASSES = {
|
||||||
|
OpusMediaCodecInformation.VENDOR_ID: {
|
||||||
|
OpusMediaCodecInformation.CODEC_ID: OpusMediaCodecInformation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class Frame:
|
|||||||
# Not supported
|
# Not supported
|
||||||
raise NotImplementedError("extended subunit types not supported")
|
raise NotImplementedError("extended subunit types not supported")
|
||||||
|
|
||||||
if subunit_id < 5:
|
if subunit_id < 5 or subunit_id == 7:
|
||||||
opcode_offset = 2
|
opcode_offset = 2
|
||||||
elif subunit_id == 5:
|
elif subunit_id == 5:
|
||||||
# Extended to the next byte
|
# Extended to the next byte
|
||||||
@@ -132,7 +132,6 @@ class Frame:
|
|||||||
else:
|
else:
|
||||||
subunit_id = 5 + extension
|
subunit_id = 5 + extension
|
||||||
opcode_offset = 3
|
opcode_offset = 3
|
||||||
|
|
||||||
elif subunit_id == 6:
|
elif subunit_id == 6:
|
||||||
raise core.InvalidPacketError("reserved subunit ID")
|
raise core.InvalidPacketError("reserved subunit ID")
|
||||||
|
|
||||||
|
|||||||
151
bumble/avdtp.py
151
bumble/avdtp.py
@@ -17,12 +17,10 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import struct
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import enum
|
import enum
|
||||||
import warnings
|
import warnings
|
||||||
from pyee import EventEmitter
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Awaitable,
|
Awaitable,
|
||||||
@@ -39,6 +37,8 @@ from typing import (
|
|||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from pyee import EventEmitter
|
||||||
|
|
||||||
from .core import (
|
from .core import (
|
||||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||||
InvalidStateError,
|
InvalidStateError,
|
||||||
@@ -51,13 +51,16 @@ from .a2dp import (
|
|||||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
A2DP_NON_A2DP_CODEC_TYPE,
|
A2DP_NON_A2DP_CODEC_TYPE,
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
|
A2DP_VENDOR_MEDIA_CODEC_INFORMATION_CLASSES,
|
||||||
AacMediaCodecInformation,
|
AacMediaCodecInformation,
|
||||||
SbcMediaCodecInformation,
|
SbcMediaCodecInformation,
|
||||||
VendorSpecificMediaCodecInformation,
|
VendorSpecificMediaCodecInformation,
|
||||||
)
|
)
|
||||||
|
from .rtp import MediaPacket
|
||||||
from . import sdp, device, l2cap
|
from . import sdp, device, l2cap
|
||||||
from .colors import color
|
from .colors import color
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -278,95 +281,6 @@ class RealtimeClock:
|
|||||||
await asyncio.sleep(duration)
|
await asyncio.sleep(duration)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
class MediaPacket:
|
|
||||||
@staticmethod
|
|
||||||
def from_bytes(data: bytes) -> MediaPacket:
|
|
||||||
version = (data[0] >> 6) & 0x03
|
|
||||||
padding = (data[0] >> 5) & 0x01
|
|
||||||
extension = (data[0] >> 4) & 0x01
|
|
||||||
csrc_count = data[0] & 0x0F
|
|
||||||
marker = (data[1] >> 7) & 0x01
|
|
||||||
payload_type = data[1] & 0x7F
|
|
||||||
sequence_number = struct.unpack_from('>H', data, 2)[0]
|
|
||||||
timestamp = struct.unpack_from('>I', data, 4)[0]
|
|
||||||
ssrc = struct.unpack_from('>I', data, 8)[0]
|
|
||||||
csrc_list = [
|
|
||||||
struct.unpack_from('>I', data, 12 + i)[0] for i in range(csrc_count)
|
|
||||||
]
|
|
||||||
payload = data[12 + csrc_count * 4 :]
|
|
||||||
|
|
||||||
return MediaPacket(
|
|
||||||
version,
|
|
||||||
padding,
|
|
||||||
extension,
|
|
||||||
marker,
|
|
||||||
sequence_number,
|
|
||||||
timestamp,
|
|
||||||
ssrc,
|
|
||||||
csrc_list,
|
|
||||||
payload_type,
|
|
||||||
payload,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
version: int,
|
|
||||||
padding: int,
|
|
||||||
extension: int,
|
|
||||||
marker: int,
|
|
||||||
sequence_number: int,
|
|
||||||
timestamp: int,
|
|
||||||
ssrc: int,
|
|
||||||
csrc_list: List[int],
|
|
||||||
payload_type: int,
|
|
||||||
payload: bytes,
|
|
||||||
) -> None:
|
|
||||||
self.version = version
|
|
||||||
self.padding = padding
|
|
||||||
self.extension = extension
|
|
||||||
self.marker = marker
|
|
||||||
self.sequence_number = sequence_number & 0xFFFF
|
|
||||||
self.timestamp = timestamp & 0xFFFFFFFF
|
|
||||||
self.ssrc = ssrc
|
|
||||||
self.csrc_list = csrc_list
|
|
||||||
self.payload_type = payload_type
|
|
||||||
self.payload = payload
|
|
||||||
|
|
||||||
def __bytes__(self) -> bytes:
|
|
||||||
header = bytes(
|
|
||||||
[
|
|
||||||
self.version << 6
|
|
||||||
| self.padding << 5
|
|
||||||
| self.extension << 4
|
|
||||||
| len(self.csrc_list),
|
|
||||||
self.marker << 7 | self.payload_type,
|
|
||||||
]
|
|
||||||
) + struct.pack(
|
|
||||||
'>HII',
|
|
||||||
self.sequence_number,
|
|
||||||
self.timestamp,
|
|
||||||
self.ssrc,
|
|
||||||
)
|
|
||||||
for csrc in self.csrc_list:
|
|
||||||
header += struct.pack('>I', csrc)
|
|
||||||
return header + self.payload
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return (
|
|
||||||
f'RTP(v={self.version},'
|
|
||||||
f'p={self.padding},'
|
|
||||||
f'x={self.extension},'
|
|
||||||
f'm={self.marker},'
|
|
||||||
f'pt={self.payload_type},'
|
|
||||||
f'sn={self.sequence_number},'
|
|
||||||
f'ts={self.timestamp},'
|
|
||||||
f'ssrc={self.ssrc},'
|
|
||||||
f'csrcs={self.csrc_list},'
|
|
||||||
f'payload_size={len(self.payload)})'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class MediaPacketPump:
|
class MediaPacketPump:
|
||||||
pump_task: Optional[asyncio.Task]
|
pump_task: Optional[asyncio.Task]
|
||||||
@@ -377,6 +291,7 @@ class MediaPacketPump:
|
|||||||
self.packets = packets
|
self.packets = packets
|
||||||
self.clock = clock
|
self.clock = clock
|
||||||
self.pump_task = None
|
self.pump_task = None
|
||||||
|
self.completed = asyncio.Event()
|
||||||
|
|
||||||
async def start(self, rtp_channel: l2cap.ClassicChannel) -> None:
|
async def start(self, rtp_channel: l2cap.ClassicChannel) -> None:
|
||||||
async def pump_packets():
|
async def pump_packets():
|
||||||
@@ -406,6 +321,8 @@ class MediaPacketPump:
|
|||||||
)
|
)
|
||||||
except asyncio.exceptions.CancelledError:
|
except asyncio.exceptions.CancelledError:
|
||||||
logger.debug('pump canceled')
|
logger.debug('pump canceled')
|
||||||
|
finally:
|
||||||
|
self.completed.set()
|
||||||
|
|
||||||
# Pump packets
|
# Pump packets
|
||||||
self.pump_task = asyncio.create_task(pump_packets())
|
self.pump_task = asyncio.create_task(pump_packets())
|
||||||
@@ -417,6 +334,9 @@ class MediaPacketPump:
|
|||||||
await self.pump_task
|
await self.pump_task
|
||||||
self.pump_task = None
|
self.pump_task = None
|
||||||
|
|
||||||
|
async def wait_for_completion(self) -> None:
|
||||||
|
await self.completed.wait()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class MessageAssembler:
|
class MessageAssembler:
|
||||||
@@ -615,11 +535,25 @@ class MediaCodecCapabilities(ServiceCapabilities):
|
|||||||
self.media_codec_information
|
self.media_codec_information
|
||||||
)
|
)
|
||||||
elif self.media_codec_type == A2DP_NON_A2DP_CODEC_TYPE:
|
elif self.media_codec_type == A2DP_NON_A2DP_CODEC_TYPE:
|
||||||
self.media_codec_information = (
|
vendor_media_codec_information = (
|
||||||
VendorSpecificMediaCodecInformation.from_bytes(
|
VendorSpecificMediaCodecInformation.from_bytes(
|
||||||
self.media_codec_information
|
self.media_codec_information
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
vendor_class_map := A2DP_VENDOR_MEDIA_CODEC_INFORMATION_CLASSES.get(
|
||||||
|
vendor_media_codec_information.vendor_id
|
||||||
|
)
|
||||||
|
) and (
|
||||||
|
media_codec_information_class := vendor_class_map.get(
|
||||||
|
vendor_media_codec_information.codec_id
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self.media_codec_information = media_codec_information_class.from_bytes(
|
||||||
|
vendor_media_codec_information.value
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.media_codec_information = vendor_media_codec_information
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -1316,10 +1250,20 @@ class Protocol(EventEmitter):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def add_source(
|
def add_source(
|
||||||
self, codec_capabilities: MediaCodecCapabilities, packet_pump: MediaPacketPump
|
self,
|
||||||
|
codec_capabilities: MediaCodecCapabilities,
|
||||||
|
packet_pump: MediaPacketPump,
|
||||||
|
delay_reporting: bool = False,
|
||||||
) -> LocalSource:
|
) -> LocalSource:
|
||||||
seid = len(self.local_endpoints) + 1
|
seid = len(self.local_endpoints) + 1
|
||||||
source = LocalSource(self, seid, codec_capabilities, packet_pump)
|
service_capabilities = (
|
||||||
|
[ServiceCapabilities(AVDTP_DELAY_REPORTING_SERVICE_CATEGORY)]
|
||||||
|
if delay_reporting
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
source = LocalSource(
|
||||||
|
self, seid, codec_capabilities, service_capabilities, packet_pump
|
||||||
|
)
|
||||||
self.local_endpoints.append(source)
|
self.local_endpoints.append(source)
|
||||||
|
|
||||||
return source
|
return source
|
||||||
@@ -1372,7 +1316,7 @@ class Protocol(EventEmitter):
|
|||||||
return self.remote_endpoints.values()
|
return self.remote_endpoints.values()
|
||||||
|
|
||||||
def find_remote_sink_by_codec(
|
def find_remote_sink_by_codec(
|
||||||
self, media_type: int, codec_type: int
|
self, media_type: int, codec_type: int, vendor_id: int = 0, codec_id: int = 0
|
||||||
) -> Optional[DiscoveredStreamEndPoint]:
|
) -> Optional[DiscoveredStreamEndPoint]:
|
||||||
for endpoint in self.remote_endpoints.values():
|
for endpoint in self.remote_endpoints.values():
|
||||||
if (
|
if (
|
||||||
@@ -1397,7 +1341,19 @@ class Protocol(EventEmitter):
|
|||||||
codec_capabilities.media_type == AVDTP_AUDIO_MEDIA_TYPE
|
codec_capabilities.media_type == AVDTP_AUDIO_MEDIA_TYPE
|
||||||
and codec_capabilities.media_codec_type == codec_type
|
and codec_capabilities.media_codec_type == codec_type
|
||||||
):
|
):
|
||||||
has_codec = True
|
if isinstance(
|
||||||
|
codec_capabilities.media_codec_information,
|
||||||
|
VendorSpecificMediaCodecInformation,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
codec_capabilities.media_codec_information.vendor_id
|
||||||
|
== vendor_id
|
||||||
|
and codec_capabilities.media_codec_information.codec_id
|
||||||
|
== codec_id
|
||||||
|
):
|
||||||
|
has_codec = True
|
||||||
|
else:
|
||||||
|
has_codec = True
|
||||||
if has_media_transport and has_codec:
|
if has_media_transport and has_codec:
|
||||||
return endpoint
|
return endpoint
|
||||||
|
|
||||||
@@ -2180,12 +2136,13 @@ class LocalSource(LocalStreamEndPoint):
|
|||||||
protocol: Protocol,
|
protocol: Protocol,
|
||||||
seid: int,
|
seid: int,
|
||||||
codec_capabilities: MediaCodecCapabilities,
|
codec_capabilities: MediaCodecCapabilities,
|
||||||
|
other_capabilitiles: Iterable[ServiceCapabilities],
|
||||||
packet_pump: MediaPacketPump,
|
packet_pump: MediaPacketPump,
|
||||||
) -> None:
|
) -> None:
|
||||||
capabilities = [
|
capabilities = [
|
||||||
ServiceCapabilities(AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY),
|
ServiceCapabilities(AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY),
|
||||||
codec_capabilities,
|
codec_capabilities,
|
||||||
]
|
] + list(other_capabilitiles)
|
||||||
super().__init__(
|
super().__init__(
|
||||||
protocol,
|
protocol,
|
||||||
seid,
|
seid,
|
||||||
|
|||||||
@@ -1491,10 +1491,14 @@ class Protocol(pyee.EventEmitter):
|
|||||||
f"<<< AVCTP Command, transaction_label={transaction_label}: " f"{command}"
|
f"<<< AVCTP Command, transaction_label={transaction_label}: " f"{command}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only the PANEL subunit type with subunit ID 0 is supported in this profile.
|
# Only addressing the unit, or the PANEL subunit with subunit ID 0 is supported
|
||||||
if (
|
# in this profile.
|
||||||
command.subunit_type != avc.Frame.SubunitType.PANEL
|
if not (
|
||||||
or command.subunit_id != 0
|
command.subunit_type == avc.Frame.SubunitType.UNIT
|
||||||
|
and command.subunit_id == 7
|
||||||
|
) and not (
|
||||||
|
command.subunit_type == avc.Frame.SubunitType.PANEL
|
||||||
|
and command.subunit_id == 0
|
||||||
):
|
):
|
||||||
logger.debug("subunit not supported")
|
logger.debug("subunit not supported")
|
||||||
self.send_not_implemented_response(transaction_label, command)
|
self.send_not_implemented_response(transaction_label, command)
|
||||||
@@ -1528,8 +1532,8 @@ class Protocol(pyee.EventEmitter):
|
|||||||
# TODO: delegate
|
# TODO: delegate
|
||||||
response = avc.PassThroughResponseFrame(
|
response = avc.PassThroughResponseFrame(
|
||||||
avc.ResponseFrame.ResponseCode.ACCEPTED,
|
avc.ResponseFrame.ResponseCode.ACCEPTED,
|
||||||
avc.Frame.SubunitType.PANEL,
|
command.subunit_type,
|
||||||
0,
|
command.subunit_id,
|
||||||
command.state_flag,
|
command.state_flag,
|
||||||
command.operation_id,
|
command.operation_id,
|
||||||
command.operation_data,
|
command.operation_data,
|
||||||
@@ -1846,6 +1850,15 @@ class Protocol(pyee.EventEmitter):
|
|||||||
RejectedResponse(pdu_id, status_code),
|
RejectedResponse(pdu_id, status_code),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def send_not_implemented_avrcp_response(
|
||||||
|
self, transaction_label: int, pdu_id: Protocol.PduId
|
||||||
|
) -> None:
|
||||||
|
self.send_avrcp_response(
|
||||||
|
transaction_label,
|
||||||
|
avc.ResponseFrame.ResponseCode.NOT_IMPLEMENTED,
|
||||||
|
NotImplementedResponse(pdu_id, b''),
|
||||||
|
)
|
||||||
|
|
||||||
def _on_get_capabilities_command(
|
def _on_get_capabilities_command(
|
||||||
self, transaction_label: int, command: GetCapabilitiesCommand
|
self, transaction_label: int, command: GetCapabilitiesCommand
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1891,29 +1904,35 @@ class Protocol(pyee.EventEmitter):
|
|||||||
async def register_notification():
|
async def register_notification():
|
||||||
# Check if the event is supported.
|
# Check if the event is supported.
|
||||||
supported_events = await self.delegate.get_supported_events()
|
supported_events = await self.delegate.get_supported_events()
|
||||||
if command.event_id in supported_events:
|
if command.event_id not in supported_events:
|
||||||
if command.event_id == EventId.VOLUME_CHANGED:
|
logger.debug("event not supported")
|
||||||
volume = await self.delegate.get_absolute_volume()
|
self.send_not_implemented_avrcp_response(
|
||||||
response = RegisterNotificationResponse(VolumeChangedEvent(volume))
|
transaction_label, self.PduId.REGISTER_NOTIFICATION
|
||||||
self.send_avrcp_response(
|
)
|
||||||
transaction_label,
|
return
|
||||||
avc.ResponseFrame.ResponseCode.INTERIM,
|
|
||||||
response,
|
|
||||||
)
|
|
||||||
self._register_notification_listener(transaction_label, command)
|
|
||||||
return
|
|
||||||
|
|
||||||
if command.event_id == EventId.PLAYBACK_STATUS_CHANGED:
|
if command.event_id == EventId.VOLUME_CHANGED:
|
||||||
# TODO: testing only, use delegate
|
volume = await self.delegate.get_absolute_volume()
|
||||||
response = RegisterNotificationResponse(
|
response = RegisterNotificationResponse(VolumeChangedEvent(volume))
|
||||||
PlaybackStatusChangedEvent(play_status=PlayStatus.PLAYING)
|
self.send_avrcp_response(
|
||||||
)
|
transaction_label,
|
||||||
self.send_avrcp_response(
|
avc.ResponseFrame.ResponseCode.INTERIM,
|
||||||
transaction_label,
|
response,
|
||||||
avc.ResponseFrame.ResponseCode.INTERIM,
|
)
|
||||||
response,
|
self._register_notification_listener(transaction_label, command)
|
||||||
)
|
return
|
||||||
self._register_notification_listener(transaction_label, command)
|
|
||||||
return
|
if command.event_id == EventId.PLAYBACK_STATUS_CHANGED:
|
||||||
|
# TODO: testing only, use delegate
|
||||||
|
response = RegisterNotificationResponse(
|
||||||
|
PlaybackStatusChangedEvent(play_status=PlayStatus.PLAYING)
|
||||||
|
)
|
||||||
|
self.send_avrcp_response(
|
||||||
|
transaction_label,
|
||||||
|
avc.ResponseFrame.ResponseCode.INTERIM,
|
||||||
|
response,
|
||||||
|
)
|
||||||
|
self._register_notification_listener(transaction_label, command)
|
||||||
|
return
|
||||||
|
|
||||||
self._delegate_command(transaction_label, command, register_notification())
|
self._delegate_command(transaction_label, command, register_notification())
|
||||||
|
|||||||
290
bumble/codecs.py
290
bumble/codecs.py
@@ -17,6 +17,7 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
from bumble import core
|
from bumble import core
|
||||||
|
|
||||||
@@ -101,12 +102,40 @@ class BitReader:
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class BitWriter:
|
||||||
|
"""Simple but not optimized bit stream writer."""
|
||||||
|
|
||||||
|
data: int
|
||||||
|
bit_count: int
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.data = 0
|
||||||
|
self.bit_count = 0
|
||||||
|
|
||||||
|
def write(self, value: int, bit_count: int) -> None:
|
||||||
|
self.data = (self.data << bit_count) | value
|
||||||
|
self.bit_count += bit_count
|
||||||
|
|
||||||
|
def write_bytes(self, data: bytes) -> None:
|
||||||
|
bit_count = 8 * len(data)
|
||||||
|
self.data = (self.data << bit_count) | int.from_bytes(data, 'big')
|
||||||
|
self.bit_count += bit_count
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
return (self.data << ((8 - (self.bit_count % 8)) % 8)).to_bytes(
|
||||||
|
(self.bit_count + 7) // 8, 'big'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AacAudioRtpPacket:
|
class AacAudioRtpPacket:
|
||||||
"""AAC payload encapsulated in an RTP packet payload"""
|
"""AAC payload encapsulated in an RTP packet payload"""
|
||||||
|
|
||||||
|
audio_mux_element: AudioMuxElement
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def latm_value(reader: BitReader) -> int:
|
def read_latm_value(reader: BitReader) -> int:
|
||||||
bytes_for_value = reader.read(2)
|
bytes_for_value = reader.read(2)
|
||||||
value = 0
|
value = 0
|
||||||
for _ in range(bytes_for_value + 1):
|
for _ in range(bytes_for_value + 1):
|
||||||
@@ -114,24 +143,33 @@ class AacAudioRtpPacket:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def program_config_element(reader: BitReader):
|
def read_audio_object_type(reader: BitReader):
|
||||||
raise core.InvalidPacketError('program_config_element not supported')
|
# GetAudioObjectType - ISO/EIC 14496-3 Table 1.16
|
||||||
|
audio_object_type = reader.read(5)
|
||||||
|
if audio_object_type == 31:
|
||||||
|
audio_object_type = 32 + reader.read(6)
|
||||||
|
|
||||||
|
return audio_object_type
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GASpecificConfig:
|
class GASpecificConfig:
|
||||||
def __init__(
|
audio_object_type: int
|
||||||
self, reader: BitReader, channel_configuration: int, audio_object_type: int
|
# NOTE: other fields not supported
|
||||||
) -> None:
|
|
||||||
|
@classmethod
|
||||||
|
def from_bits(
|
||||||
|
cls, reader: BitReader, channel_configuration: int, audio_object_type: int
|
||||||
|
) -> Self:
|
||||||
# GASpecificConfig - ISO/EIC 14496-3 Table 4.1
|
# GASpecificConfig - ISO/EIC 14496-3 Table 4.1
|
||||||
frame_length_flag = reader.read(1)
|
frame_length_flag = reader.read(1)
|
||||||
depends_on_core_coder = reader.read(1)
|
depends_on_core_coder = reader.read(1)
|
||||||
if depends_on_core_coder:
|
if depends_on_core_coder:
|
||||||
self.core_coder_delay = reader.read(14)
|
core_coder_delay = reader.read(14)
|
||||||
extension_flag = reader.read(1)
|
extension_flag = reader.read(1)
|
||||||
if not channel_configuration:
|
if not channel_configuration:
|
||||||
AacAudioRtpPacket.program_config_element(reader)
|
raise core.InvalidPacketError('program_config_element not supported')
|
||||||
if audio_object_type in (6, 20):
|
if audio_object_type in (6, 20):
|
||||||
self.layer_nr = reader.read(3)
|
layer_nr = reader.read(3)
|
||||||
if extension_flag:
|
if extension_flag:
|
||||||
if audio_object_type == 22:
|
if audio_object_type == 22:
|
||||||
num_of_sub_frame = reader.read(5)
|
num_of_sub_frame = reader.read(5)
|
||||||
@@ -144,14 +182,13 @@ class AacAudioRtpPacket:
|
|||||||
if extension_flag_3 == 1:
|
if extension_flag_3 == 1:
|
||||||
raise core.InvalidPacketError('extensionFlag3 == 1 not supported')
|
raise core.InvalidPacketError('extensionFlag3 == 1 not supported')
|
||||||
|
|
||||||
@staticmethod
|
return cls(audio_object_type)
|
||||||
def audio_object_type(reader: BitReader):
|
|
||||||
# GetAudioObjectType - ISO/EIC 14496-3 Table 1.16
|
|
||||||
audio_object_type = reader.read(5)
|
|
||||||
if audio_object_type == 31:
|
|
||||||
audio_object_type = 32 + reader.read(6)
|
|
||||||
|
|
||||||
return audio_object_type
|
def to_bits(self, writer: BitWriter) -> None:
|
||||||
|
assert self.audio_object_type in (1, 2)
|
||||||
|
writer.write(0, 1) # frame_length_flag = 0
|
||||||
|
writer.write(0, 1) # depends_on_core_coder = 0
|
||||||
|
writer.write(0, 1) # extension_flag = 0
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AudioSpecificConfig:
|
class AudioSpecificConfig:
|
||||||
@@ -159,6 +196,7 @@ class AacAudioRtpPacket:
|
|||||||
sampling_frequency_index: int
|
sampling_frequency_index: int
|
||||||
sampling_frequency: int
|
sampling_frequency: int
|
||||||
channel_configuration: int
|
channel_configuration: int
|
||||||
|
ga_specific_config: AacAudioRtpPacket.GASpecificConfig
|
||||||
sbr_present_flag: int
|
sbr_present_flag: int
|
||||||
ps_present_flag: int
|
ps_present_flag: int
|
||||||
extension_audio_object_type: int
|
extension_audio_object_type: int
|
||||||
@@ -182,44 +220,73 @@ class AacAudioRtpPacket:
|
|||||||
7350,
|
7350,
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, reader: BitReader) -> None:
|
@classmethod
|
||||||
# AudioSpecificConfig - ISO/EIC 14496-3 Table 1.15
|
def for_simple_aac(
|
||||||
self.audio_object_type = AacAudioRtpPacket.audio_object_type(reader)
|
cls,
|
||||||
self.sampling_frequency_index = reader.read(4)
|
audio_object_type: int,
|
||||||
if self.sampling_frequency_index == 0xF:
|
sampling_frequency: int,
|
||||||
self.sampling_frequency = reader.read(24)
|
channel_configuration: int,
|
||||||
else:
|
) -> Self:
|
||||||
self.sampling_frequency = self.SAMPLING_FREQUENCIES[
|
if sampling_frequency not in cls.SAMPLING_FREQUENCIES:
|
||||||
self.sampling_frequency_index
|
raise ValueError(f'invalid sampling frequency {sampling_frequency}')
|
||||||
]
|
|
||||||
self.channel_configuration = reader.read(4)
|
|
||||||
self.sbr_present_flag = -1
|
|
||||||
self.ps_present_flag = -1
|
|
||||||
if self.audio_object_type in (5, 29):
|
|
||||||
self.extension_audio_object_type = 5
|
|
||||||
self.sbc_present_flag = 1
|
|
||||||
if self.audio_object_type == 29:
|
|
||||||
self.ps_present_flag = 1
|
|
||||||
self.extension_sampling_frequency_index = reader.read(4)
|
|
||||||
if self.extension_sampling_frequency_index == 0xF:
|
|
||||||
self.extension_sampling_frequency = reader.read(24)
|
|
||||||
else:
|
|
||||||
self.extension_sampling_frequency = self.SAMPLING_FREQUENCIES[
|
|
||||||
self.extension_sampling_frequency_index
|
|
||||||
]
|
|
||||||
self.audio_object_type = AacAudioRtpPacket.audio_object_type(reader)
|
|
||||||
if self.audio_object_type == 22:
|
|
||||||
self.extension_channel_configuration = reader.read(4)
|
|
||||||
else:
|
|
||||||
self.extension_audio_object_type = 0
|
|
||||||
|
|
||||||
if self.audio_object_type in (1, 2, 3, 4, 6, 7, 17, 19, 20, 21, 22, 23):
|
ga_specific_config = AacAudioRtpPacket.GASpecificConfig(audio_object_type)
|
||||||
ga_specific_config = AacAudioRtpPacket.GASpecificConfig(
|
|
||||||
reader, self.channel_configuration, self.audio_object_type
|
return cls(
|
||||||
|
audio_object_type=audio_object_type,
|
||||||
|
sampling_frequency_index=cls.SAMPLING_FREQUENCIES.index(
|
||||||
|
sampling_frequency
|
||||||
|
),
|
||||||
|
sampling_frequency=sampling_frequency,
|
||||||
|
channel_configuration=channel_configuration,
|
||||||
|
ga_specific_config=ga_specific_config,
|
||||||
|
sbr_present_flag=0,
|
||||||
|
ps_present_flag=0,
|
||||||
|
extension_audio_object_type=0,
|
||||||
|
extension_sampling_frequency_index=0,
|
||||||
|
extension_sampling_frequency=0,
|
||||||
|
extension_channel_configuration=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bits(cls, reader: BitReader) -> Self:
|
||||||
|
# AudioSpecificConfig - ISO/EIC 14496-3 Table 1.15
|
||||||
|
audio_object_type = AacAudioRtpPacket.read_audio_object_type(reader)
|
||||||
|
sampling_frequency_index = reader.read(4)
|
||||||
|
if sampling_frequency_index == 0xF:
|
||||||
|
sampling_frequency = reader.read(24)
|
||||||
|
else:
|
||||||
|
sampling_frequency = cls.SAMPLING_FREQUENCIES[sampling_frequency_index]
|
||||||
|
channel_configuration = reader.read(4)
|
||||||
|
sbr_present_flag = 0
|
||||||
|
ps_present_flag = 0
|
||||||
|
extension_sampling_frequency_index = 0
|
||||||
|
extension_sampling_frequency = 0
|
||||||
|
extension_channel_configuration = 0
|
||||||
|
extension_audio_object_type = 0
|
||||||
|
if audio_object_type in (5, 29):
|
||||||
|
extension_audio_object_type = 5
|
||||||
|
sbr_present_flag = 1
|
||||||
|
if audio_object_type == 29:
|
||||||
|
ps_present_flag = 1
|
||||||
|
extension_sampling_frequency_index = reader.read(4)
|
||||||
|
if extension_sampling_frequency_index == 0xF:
|
||||||
|
extension_sampling_frequency = reader.read(24)
|
||||||
|
else:
|
||||||
|
extension_sampling_frequency = cls.SAMPLING_FREQUENCIES[
|
||||||
|
extension_sampling_frequency_index
|
||||||
|
]
|
||||||
|
audio_object_type = AacAudioRtpPacket.read_audio_object_type(reader)
|
||||||
|
if audio_object_type == 22:
|
||||||
|
extension_channel_configuration = reader.read(4)
|
||||||
|
|
||||||
|
if audio_object_type in (1, 2, 3, 4, 6, 7, 17, 19, 20, 21, 22, 23):
|
||||||
|
ga_specific_config = AacAudioRtpPacket.GASpecificConfig.from_bits(
|
||||||
|
reader, channel_configuration, audio_object_type
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise core.InvalidPacketError(
|
raise core.InvalidPacketError(
|
||||||
f'audioObjectType {self.audio_object_type} not supported'
|
f'audioObjectType {audio_object_type} not supported'
|
||||||
)
|
)
|
||||||
|
|
||||||
# if self.extension_audio_object_type != 5 and bits_to_decode >= 16:
|
# if self.extension_audio_object_type != 5 and bits_to_decode >= 16:
|
||||||
@@ -248,13 +315,44 @@ class AacAudioRtpPacket:
|
|||||||
# self.extension_sampling_frequency = self.SAMPLING_FREQUENCIES[self.extension_sampling_frequency_index]
|
# self.extension_sampling_frequency = self.SAMPLING_FREQUENCIES[self.extension_sampling_frequency_index]
|
||||||
# self.extension_channel_configuration = reader.read(4)
|
# self.extension_channel_configuration = reader.read(4)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
audio_object_type,
|
||||||
|
sampling_frequency_index,
|
||||||
|
sampling_frequency,
|
||||||
|
channel_configuration,
|
||||||
|
ga_specific_config,
|
||||||
|
sbr_present_flag,
|
||||||
|
ps_present_flag,
|
||||||
|
extension_audio_object_type,
|
||||||
|
extension_sampling_frequency_index,
|
||||||
|
extension_sampling_frequency,
|
||||||
|
extension_channel_configuration,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_bits(self, writer: BitWriter) -> None:
|
||||||
|
if self.sampling_frequency_index >= 15:
|
||||||
|
raise ValueError(
|
||||||
|
f"unsupported sampling frequency index {self.sampling_frequency_index}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.audio_object_type not in (1, 2):
|
||||||
|
raise ValueError(
|
||||||
|
f"unsupported audio object type {self.audio_object_type} "
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.write(self.audio_object_type, 5)
|
||||||
|
writer.write(self.sampling_frequency_index, 4)
|
||||||
|
writer.write(self.channel_configuration, 4)
|
||||||
|
self.ga_specific_config.to_bits(writer)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class StreamMuxConfig:
|
class StreamMuxConfig:
|
||||||
other_data_present: int
|
other_data_present: int
|
||||||
other_data_len_bits: int
|
other_data_len_bits: int
|
||||||
audio_specific_config: AacAudioRtpPacket.AudioSpecificConfig
|
audio_specific_config: AacAudioRtpPacket.AudioSpecificConfig
|
||||||
|
|
||||||
def __init__(self, reader: BitReader) -> None:
|
@classmethod
|
||||||
|
def from_bits(cls, reader: BitReader) -> Self:
|
||||||
# StreamMuxConfig - ISO/EIC 14496-3 Table 1.42
|
# StreamMuxConfig - ISO/EIC 14496-3 Table 1.42
|
||||||
audio_mux_version = reader.read(1)
|
audio_mux_version = reader.read(1)
|
||||||
if audio_mux_version == 1:
|
if audio_mux_version == 1:
|
||||||
@@ -264,7 +362,7 @@ class AacAudioRtpPacket:
|
|||||||
if audio_mux_version_a != 0:
|
if audio_mux_version_a != 0:
|
||||||
raise core.InvalidPacketError('audioMuxVersionA != 0 not supported')
|
raise core.InvalidPacketError('audioMuxVersionA != 0 not supported')
|
||||||
if audio_mux_version == 1:
|
if audio_mux_version == 1:
|
||||||
tara_buffer_fullness = AacAudioRtpPacket.latm_value(reader)
|
tara_buffer_fullness = AacAudioRtpPacket.read_latm_value(reader)
|
||||||
stream_cnt = 0
|
stream_cnt = 0
|
||||||
all_streams_same_time_framing = reader.read(1)
|
all_streams_same_time_framing = reader.read(1)
|
||||||
num_sub_frames = reader.read(6)
|
num_sub_frames = reader.read(6)
|
||||||
@@ -275,13 +373,13 @@ class AacAudioRtpPacket:
|
|||||||
if num_layer != 0:
|
if num_layer != 0:
|
||||||
raise core.InvalidPacketError('num_layer != 0 not supported')
|
raise core.InvalidPacketError('num_layer != 0 not supported')
|
||||||
if audio_mux_version == 0:
|
if audio_mux_version == 0:
|
||||||
self.audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig(
|
audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig.from_bits(
|
||||||
reader
|
reader
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
asc_len = AacAudioRtpPacket.latm_value(reader)
|
asc_len = AacAudioRtpPacket.read_latm_value(reader)
|
||||||
marker = reader.bit_position
|
marker = reader.bit_position
|
||||||
self.audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig(
|
audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig.from_bits(
|
||||||
reader
|
reader
|
||||||
)
|
)
|
||||||
audio_specific_config_len = reader.bit_position - marker
|
audio_specific_config_len = reader.bit_position - marker
|
||||||
@@ -299,36 +397,49 @@ class AacAudioRtpPacket:
|
|||||||
f'frame_length_type {frame_length_type} not supported'
|
f'frame_length_type {frame_length_type} not supported'
|
||||||
)
|
)
|
||||||
|
|
||||||
self.other_data_present = reader.read(1)
|
other_data_present = reader.read(1)
|
||||||
if self.other_data_present:
|
other_data_len_bits = 0
|
||||||
|
if other_data_present:
|
||||||
if audio_mux_version == 1:
|
if audio_mux_version == 1:
|
||||||
self.other_data_len_bits = AacAudioRtpPacket.latm_value(reader)
|
other_data_len_bits = AacAudioRtpPacket.read_latm_value(reader)
|
||||||
else:
|
else:
|
||||||
self.other_data_len_bits = 0
|
|
||||||
while True:
|
while True:
|
||||||
self.other_data_len_bits *= 256
|
other_data_len_bits *= 256
|
||||||
other_data_len_esc = reader.read(1)
|
other_data_len_esc = reader.read(1)
|
||||||
self.other_data_len_bits += reader.read(8)
|
other_data_len_bits += reader.read(8)
|
||||||
if other_data_len_esc == 0:
|
if other_data_len_esc == 0:
|
||||||
break
|
break
|
||||||
crc_check_present = reader.read(1)
|
crc_check_present = reader.read(1)
|
||||||
if crc_check_present:
|
if crc_check_present:
|
||||||
crc_checksum = reader.read(8)
|
crc_checksum = reader.read(8)
|
||||||
|
|
||||||
|
return cls(other_data_present, other_data_len_bits, audio_specific_config)
|
||||||
|
|
||||||
|
def to_bits(self, writer: BitWriter) -> None:
|
||||||
|
writer.write(0, 1) # audioMuxVersion = 0
|
||||||
|
writer.write(1, 1) # allStreamsSameTimeFraming = 1
|
||||||
|
writer.write(0, 6) # numSubFrames = 0
|
||||||
|
writer.write(0, 4) # numProgram = 0
|
||||||
|
writer.write(0, 3) # numLayer = 0
|
||||||
|
self.audio_specific_config.to_bits(writer)
|
||||||
|
writer.write(0, 3) # frameLengthType = 0
|
||||||
|
writer.write(0, 8) # latmBufferFullness = 0
|
||||||
|
writer.write(0, 1) # otherDataPresent = 0
|
||||||
|
writer.write(0, 1) # crcCheckPresent = 0
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AudioMuxElement:
|
class AudioMuxElement:
|
||||||
payload: bytes
|
|
||||||
stream_mux_config: AacAudioRtpPacket.StreamMuxConfig
|
stream_mux_config: AacAudioRtpPacket.StreamMuxConfig
|
||||||
|
payload: bytes
|
||||||
|
|
||||||
def __init__(self, reader: BitReader, mux_config_present: int):
|
@classmethod
|
||||||
if mux_config_present == 0:
|
def from_bits(cls, reader: BitReader) -> Self:
|
||||||
raise core.InvalidPacketError('muxConfigPresent == 0 not supported')
|
|
||||||
|
|
||||||
# AudioMuxElement - ISO/EIC 14496-3 Table 1.41
|
# AudioMuxElement - ISO/EIC 14496-3 Table 1.41
|
||||||
|
# (only supports mux_config_present=1)
|
||||||
use_same_stream_mux = reader.read(1)
|
use_same_stream_mux = reader.read(1)
|
||||||
if use_same_stream_mux:
|
if use_same_stream_mux:
|
||||||
raise core.InvalidPacketError('useSameStreamMux == 1 not supported')
|
raise core.InvalidPacketError('useSameStreamMux == 1 not supported')
|
||||||
self.stream_mux_config = AacAudioRtpPacket.StreamMuxConfig(reader)
|
stream_mux_config = AacAudioRtpPacket.StreamMuxConfig.from_bits(reader)
|
||||||
|
|
||||||
# We only support:
|
# We only support:
|
||||||
# allStreamsSameTimeFraming == 1
|
# allStreamsSameTimeFraming == 1
|
||||||
@@ -344,19 +455,46 @@ class AacAudioRtpPacket:
|
|||||||
if tmp != 255:
|
if tmp != 255:
|
||||||
break
|
break
|
||||||
|
|
||||||
self.payload = reader.read_bytes(mux_slot_length_bytes)
|
payload = reader.read_bytes(mux_slot_length_bytes)
|
||||||
|
|
||||||
if self.stream_mux_config.other_data_present:
|
if stream_mux_config.other_data_present:
|
||||||
reader.skip(self.stream_mux_config.other_data_len_bits)
|
reader.skip(stream_mux_config.other_data_len_bits)
|
||||||
|
|
||||||
# ByteAlign
|
# ByteAlign
|
||||||
while reader.bit_position % 8:
|
while reader.bit_position % 8:
|
||||||
reader.read(1)
|
reader.read(1)
|
||||||
|
|
||||||
def __init__(self, data: bytes) -> None:
|
return cls(stream_mux_config, payload)
|
||||||
|
|
||||||
|
def to_bits(self, writer: BitWriter) -> None:
|
||||||
|
writer.write(0, 1) # useSameStreamMux = 0
|
||||||
|
self.stream_mux_config.to_bits(writer)
|
||||||
|
mux_slot_length_bytes = len(self.payload)
|
||||||
|
while mux_slot_length_bytes > 255:
|
||||||
|
writer.write(255, 8)
|
||||||
|
mux_slot_length_bytes -= 255
|
||||||
|
writer.write(mux_slot_length_bytes, 8)
|
||||||
|
if mux_slot_length_bytes == 255:
|
||||||
|
writer.write(0, 8)
|
||||||
|
writer.write_bytes(self.payload)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes) -> Self:
|
||||||
# Parse the bit stream
|
# Parse the bit stream
|
||||||
reader = BitReader(data)
|
reader = BitReader(data)
|
||||||
self.audio_mux_element = self.AudioMuxElement(reader, mux_config_present=1)
|
return cls(cls.AudioMuxElement.from_bits(reader))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_simple_aac(
|
||||||
|
cls, sampling_frequency: int, channel_configuration: int, payload: bytes
|
||||||
|
) -> Self:
|
||||||
|
audio_specific_config = cls.AudioSpecificConfig.for_simple_aac(
|
||||||
|
2, sampling_frequency, channel_configuration
|
||||||
|
)
|
||||||
|
stream_mux_config = cls.StreamMuxConfig(0, 0, audio_specific_config)
|
||||||
|
audio_mux_element = cls.AudioMuxElement(stream_mux_config, payload)
|
||||||
|
|
||||||
|
return cls(audio_mux_element)
|
||||||
|
|
||||||
def to_adts(self):
|
def to_adts(self):
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
@@ -383,3 +521,11 @@ class AacAudioRtpPacket:
|
|||||||
)
|
)
|
||||||
+ self.audio_mux_element.payload
|
+ self.audio_mux_element.payload
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, audio_mux_element: AudioMuxElement) -> None:
|
||||||
|
self.audio_mux_element = audio_mux_element
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
writer = BitWriter()
|
||||||
|
self.audio_mux_element.to_bits(writer)
|
||||||
|
return bytes(writer)
|
||||||
|
|||||||
@@ -1571,14 +1571,22 @@ class Connection(CompositeEventEmitter):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return (
|
if self.transport == BT_LE_TRANSPORT:
|
||||||
f'Connection(handle=0x{self.handle:04X}, '
|
return (
|
||||||
f'role={self.role_name}, '
|
f'Connection(transport=LE, handle=0x{self.handle:04X}, '
|
||||||
f'self_address={self.self_address}, '
|
f'role={self.role_name}, '
|
||||||
f'self_resolvable_address={self.self_resolvable_address}, '
|
f'self_address={self.self_address}, '
|
||||||
f'peer_address={self.peer_address}, '
|
f'self_resolvable_address={self.self_resolvable_address}, '
|
||||||
f'peer_resolvable_address={self.peer_resolvable_address})'
|
f'peer_address={self.peer_address}, '
|
||||||
)
|
f'peer_resolvable_address={self.peer_resolvable_address})'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
f'Connection(transport=BR/EDR, handle=0x{self.handle:04X}, '
|
||||||
|
f'role={self.role_name}, '
|
||||||
|
f'self_address={self.self_address}, '
|
||||||
|
f'peer_address={self.peer_address})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -6065,6 +6065,32 @@ class HCI_Read_Remote_Version_Information_Complete_Event(HCI_Event):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Event.event(
|
||||||
|
[
|
||||||
|
('status', STATUS_SPEC),
|
||||||
|
('connection_handle', 2),
|
||||||
|
('unused', 1),
|
||||||
|
(
|
||||||
|
'service_type',
|
||||||
|
{
|
||||||
|
'size': 1,
|
||||||
|
'mapper': lambda x: HCI_QOS_Setup_Complete_Event.ServiceType(x).name,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class HCI_QOS_Setup_Complete_Event(HCI_Event):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.7.13 QoS Setup Complete Event
|
||||||
|
'''
|
||||||
|
|
||||||
|
class ServiceType(OpenIntEnum):
|
||||||
|
NO_TRAFFIC_AVAILABLE = 0x00
|
||||||
|
BEST_EFFORT_AVAILABLE = 0x01
|
||||||
|
GUARANTEED_AVAILABLE = 0x02
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Event.event(
|
@HCI_Event.event(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1106,6 +1106,18 @@ class Host(AbortableEventEmitter):
|
|||||||
event.status,
|
event.status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def on_hci_qos_setup_complete_event(self, event):
|
||||||
|
if event.status == hci.HCI_SUCCESS:
|
||||||
|
self.emit(
|
||||||
|
'connection_qos_setup', event.connection_handle, event.service_type
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.emit(
|
||||||
|
'connection_qos_setup_failure',
|
||||||
|
event.connection_handle,
|
||||||
|
event.status,
|
||||||
|
)
|
||||||
|
|
||||||
def on_hci_link_supervision_timeout_changed_event(self, event):
|
def on_hci_link_supervision_timeout_changed_event(self, event):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
110
bumble/rtp.py
Normal file
110
bumble/rtp.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# Copyright 2024 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Imports
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
from __future__ import annotations
|
||||||
|
import struct
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class MediaPacket:
|
||||||
|
@staticmethod
|
||||||
|
def from_bytes(data: bytes) -> MediaPacket:
|
||||||
|
version = (data[0] >> 6) & 0x03
|
||||||
|
padding = (data[0] >> 5) & 0x01
|
||||||
|
extension = (data[0] >> 4) & 0x01
|
||||||
|
csrc_count = data[0] & 0x0F
|
||||||
|
marker = (data[1] >> 7) & 0x01
|
||||||
|
payload_type = data[1] & 0x7F
|
||||||
|
sequence_number = struct.unpack_from('>H', data, 2)[0]
|
||||||
|
timestamp = struct.unpack_from('>I', data, 4)[0]
|
||||||
|
ssrc = struct.unpack_from('>I', data, 8)[0]
|
||||||
|
csrc_list = [
|
||||||
|
struct.unpack_from('>I', data, 12 + i)[0] for i in range(csrc_count)
|
||||||
|
]
|
||||||
|
payload = data[12 + csrc_count * 4 :]
|
||||||
|
|
||||||
|
return MediaPacket(
|
||||||
|
version,
|
||||||
|
padding,
|
||||||
|
extension,
|
||||||
|
marker,
|
||||||
|
sequence_number,
|
||||||
|
timestamp,
|
||||||
|
ssrc,
|
||||||
|
csrc_list,
|
||||||
|
payload_type,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
version: int,
|
||||||
|
padding: int,
|
||||||
|
extension: int,
|
||||||
|
marker: int,
|
||||||
|
sequence_number: int,
|
||||||
|
timestamp: int,
|
||||||
|
ssrc: int,
|
||||||
|
csrc_list: List[int],
|
||||||
|
payload_type: int,
|
||||||
|
payload: bytes,
|
||||||
|
) -> None:
|
||||||
|
self.version = version
|
||||||
|
self.padding = padding
|
||||||
|
self.extension = extension
|
||||||
|
self.marker = marker
|
||||||
|
self.sequence_number = sequence_number & 0xFFFF
|
||||||
|
self.timestamp = timestamp & 0xFFFFFFFF
|
||||||
|
self.timestamp_seconds = 0.0
|
||||||
|
self.ssrc = ssrc
|
||||||
|
self.csrc_list = csrc_list
|
||||||
|
self.payload_type = payload_type
|
||||||
|
self.payload = payload
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
header = bytes(
|
||||||
|
[
|
||||||
|
self.version << 6
|
||||||
|
| self.padding << 5
|
||||||
|
| self.extension << 4
|
||||||
|
| len(self.csrc_list),
|
||||||
|
self.marker << 7 | self.payload_type,
|
||||||
|
]
|
||||||
|
) + struct.pack(
|
||||||
|
'>HII',
|
||||||
|
self.sequence_number,
|
||||||
|
self.timestamp,
|
||||||
|
self.ssrc,
|
||||||
|
)
|
||||||
|
for csrc in self.csrc_list:
|
||||||
|
header += struct.pack('>I', csrc)
|
||||||
|
return header + self.payload
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return (
|
||||||
|
f'RTP(v={self.version},'
|
||||||
|
f'p={self.padding},'
|
||||||
|
f'x={self.extension},'
|
||||||
|
f'm={self.marker},'
|
||||||
|
f'pt={self.payload_type},'
|
||||||
|
f'sn={self.sequence_number},'
|
||||||
|
f'ts={self.timestamp},'
|
||||||
|
f'ssrc={self.ssrc},'
|
||||||
|
f'csrcs={self.csrc_list},'
|
||||||
|
f'payload_size={len(self.payload)})'
|
||||||
|
)
|
||||||
@@ -61,20 +61,23 @@ def codec_capabilities():
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channel_modes=[
|
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
SBC_MONO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
],
|
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
block_lengths=[4, 8, 12, 16],
|
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
subbands=[4, 8],
|
block_length=SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
allocation_methods=[
|
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_16,
|
||||||
],
|
subbands=SbcMediaCodecInformation.Subbands.S_4
|
||||||
|
| SbcMediaCodecInformation.Subbands.S_8,
|
||||||
|
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||||
|
| SbcMediaCodecInformation.AllocationMethod.SNR,
|
||||||
minimum_bitpool_value=2,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ from bumble.avdtp import (
|
|||||||
Listener,
|
Listener,
|
||||||
)
|
)
|
||||||
from bumble.a2dp import (
|
from bumble.a2dp import (
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
|
||||||
make_audio_source_service_sdp_records,
|
make_audio_source_service_sdp_records,
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
SbcMediaCodecInformation,
|
SbcMediaCodecInformation,
|
||||||
@@ -59,12 +57,12 @@ def codec_capabilities():
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_discrete_values(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequency=44100,
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||||
channel_mode=SBC_JOINT_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
block_length=16,
|
block_length=SbcMediaCodecInformation.BlockLength.BL_16,
|
||||||
subbands=8,
|
subbands=SbcMediaCodecInformation.Subbands.S_8,
|
||||||
allocation_method=SBC_LOUDNESS_ALLOCATION_METHOD,
|
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||||
minimum_bitpool_value=2,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
@@ -73,11 +71,9 @@ def codec_capabilities():
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def on_avdtp_connection(read_function, protocol):
|
def on_avdtp_connection(read_function, protocol):
|
||||||
packet_source = SbcPacketSource(
|
packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.peer_mtu)
|
||||||
read_function, protocol.l2cap_channel.peer_mtu, codec_capabilities()
|
|
||||||
)
|
|
||||||
packet_pump = MediaPacketPump(packet_source.packets)
|
packet_pump = MediaPacketPump(packet_source.packets)
|
||||||
protocol.add_source(packet_source.codec_capabilities, packet_pump)
|
protocol.add_source(codec_capabilities(), packet_pump)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -97,11 +93,9 @@ async def stream_packets(read_function, protocol):
|
|||||||
print(f'### Selected sink: {sink.seid}')
|
print(f'### Selected sink: {sink.seid}')
|
||||||
|
|
||||||
# Stream the packets
|
# Stream the packets
|
||||||
packet_source = SbcPacketSource(
|
packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.peer_mtu)
|
||||||
read_function, protocol.l2cap_channel.peer_mtu, codec_capabilities()
|
|
||||||
)
|
|
||||||
packet_pump = MediaPacketPump(packet_source.packets)
|
packet_pump = MediaPacketPump(packet_source.packets)
|
||||||
source = protocol.add_source(packet_source.codec_capabilities, packet_pump)
|
source = protocol.add_source(codec_capabilities(), packet_pump)
|
||||||
stream = await protocol.create_stream(source, sink)
|
stream = await protocol.create_stream(source, sink)
|
||||||
await stream.start()
|
await stream.start()
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|||||||
@@ -60,20 +60,23 @@ def codec_capabilities():
|
|||||||
return avdtp.MediaCodecCapabilities(
|
return avdtp.MediaCodecCapabilities(
|
||||||
media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=a2dp.A2DP_SBC_CODEC_TYPE,
|
media_codec_type=a2dp.A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=a2dp.SbcMediaCodecInformation.from_lists(
|
media_codec_information=a2dp.SbcMediaCodecInformation(
|
||||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channel_modes=[
|
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
a2dp.SBC_MONO_CHANNEL_MODE,
|
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||||
a2dp.SBC_DUAL_CHANNEL_MODE,
|
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||||
a2dp.SBC_STEREO_CHANNEL_MODE,
|
channel_mode=a2dp.SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
a2dp.SBC_JOINT_STEREO_CHANNEL_MODE,
|
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
],
|
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
block_lengths=[4, 8, 12, 16],
|
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
subbands=[4, 8],
|
block_length=a2dp.SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
allocation_methods=[
|
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
a2dp.SBC_LOUDNESS_ALLOCATION_METHOD,
|
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
a2dp.SBC_SNR_ALLOCATION_METHOD,
|
| a2dp.SbcMediaCodecInformation.BlockLength.BL_16,
|
||||||
],
|
subbands=a2dp.SbcMediaCodecInformation.Subbands.S_4
|
||||||
|
| a2dp.SbcMediaCodecInformation.Subbands.S_8,
|
||||||
|
allocation_method=a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||||
|
| a2dp.SbcMediaCodecInformation.AllocationMethod.SNR,
|
||||||
minimum_bitpool_value=2,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ console_scripts =
|
|||||||
bumble-usb-probe = bumble.apps.usb_probe:main
|
bumble-usb-probe = bumble.apps.usb_probe:main
|
||||||
bumble-link-relay = bumble.apps.link_relay.link_relay:main
|
bumble-link-relay = bumble.apps.link_relay.link_relay:main
|
||||||
bumble-bench = bumble.apps.bench:main
|
bumble-bench = bumble.apps.bench:main
|
||||||
|
bumble-player = bumble.apps.player.player:main
|
||||||
bumble-speaker = bumble.apps.speaker.speaker:main
|
bumble-speaker = bumble.apps.speaker.speaker:main
|
||||||
bumble-pandora-server = bumble.apps.pandora_server:main
|
bumble-pandora-server = bumble.apps.pandora_server:main
|
||||||
bumble-rtk-util = bumble.tools.rtk_util:main
|
bumble-rtk-util = bumble.tools.rtk_util:main
|
||||||
|
|||||||
@@ -33,20 +33,16 @@ from bumble.avdtp import (
|
|||||||
Protocol,
|
Protocol,
|
||||||
Listener,
|
Listener,
|
||||||
MediaCodecCapabilities,
|
MediaCodecCapabilities,
|
||||||
MediaPacket,
|
|
||||||
AVDTP_AUDIO_MEDIA_TYPE,
|
AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
AVDTP_TSEP_SNK,
|
AVDTP_TSEP_SNK,
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
)
|
)
|
||||||
from bumble.a2dp import (
|
from bumble.a2dp import (
|
||||||
|
AacMediaCodecInformation,
|
||||||
|
OpusMediaCodecInformation,
|
||||||
SbcMediaCodecInformation,
|
SbcMediaCodecInformation,
|
||||||
SBC_MONO_CHANNEL_MODE,
|
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
|
||||||
)
|
)
|
||||||
|
from bumble.rtp import MediaPacket
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -125,12 +121,12 @@ def source_codec_capabilities():
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_discrete_values(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequency=44100,
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||||
channel_mode=SBC_JOINT_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
block_length=16,
|
block_length=SbcMediaCodecInformation.BlockLength.BL_16,
|
||||||
subbands=8,
|
subbands=SbcMediaCodecInformation.Subbands.S_8,
|
||||||
allocation_method=SBC_LOUDNESS_ALLOCATION_METHOD,
|
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||||
minimum_bitpool_value=2,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
@@ -142,20 +138,23 @@ def sink_codec_capabilities():
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channel_modes=[
|
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
SBC_MONO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
],
|
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
block_lengths=[4, 8, 12, 16],
|
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
subbands=[4, 8],
|
block_length=SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
allocation_methods=[
|
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_16,
|
||||||
],
|
subbands=SbcMediaCodecInformation.Subbands.S_4
|
||||||
|
| SbcMediaCodecInformation.Subbands.S_8,
|
||||||
|
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||||
|
| SbcMediaCodecInformation.AllocationMethod.SNR,
|
||||||
minimum_bitpool_value=2,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
@@ -273,7 +272,125 @@ async def test_source_sink_1():
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def run_test_self():
|
def test_sbc_codec_specific_information():
|
||||||
|
sbc_info = SbcMediaCodecInformation.from_bytes(bytes.fromhex("3fff0235"))
|
||||||
|
assert (
|
||||||
|
sbc_info.sampling_frequency
|
||||||
|
== SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
|
| SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sbc_info.channel_mode
|
||||||
|
== SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
|
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
|
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
|
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sbc_info.block_length
|
||||||
|
== SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
|
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
|
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
|
| SbcMediaCodecInformation.BlockLength.BL_16
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sbc_info.subbands
|
||||||
|
== SbcMediaCodecInformation.Subbands.S_4 | SbcMediaCodecInformation.Subbands.S_8
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
sbc_info.allocation_method
|
||||||
|
== SbcMediaCodecInformation.AllocationMethod.SNR
|
||||||
|
| SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||||
|
)
|
||||||
|
assert sbc_info.minimum_bitpool_value == 2
|
||||||
|
assert sbc_info.maximum_bitpool_value == 53
|
||||||
|
|
||||||
|
sbc_info2 = SbcMediaCodecInformation(
|
||||||
|
SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
|
| SbcMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||||
|
SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
|
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
|
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
|
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
|
SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
|
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
|
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
|
| SbcMediaCodecInformation.BlockLength.BL_16,
|
||||||
|
SbcMediaCodecInformation.Subbands.S_4 | SbcMediaCodecInformation.Subbands.S_8,
|
||||||
|
SbcMediaCodecInformation.AllocationMethod.SNR
|
||||||
|
| SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||||
|
2,
|
||||||
|
53,
|
||||||
|
)
|
||||||
|
assert sbc_info == sbc_info2
|
||||||
|
assert bytes(sbc_info2) == bytes.fromhex("3fff0235")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_aac_codec_specific_information():
|
||||||
|
aac_info = AacMediaCodecInformation.from_bytes(bytes.fromhex("f0018c83e800"))
|
||||||
|
assert (
|
||||||
|
aac_info.object_type
|
||||||
|
== AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
|
||||||
|
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
|
||||||
|
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
|
||||||
|
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
aac_info.sampling_frequency
|
||||||
|
== AacMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
|
| AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
aac_info.channels
|
||||||
|
== AacMediaCodecInformation.Channels.MONO
|
||||||
|
| AacMediaCodecInformation.Channels.STEREO
|
||||||
|
)
|
||||||
|
assert aac_info.vbr == 1
|
||||||
|
assert aac_info.bitrate == 256000
|
||||||
|
|
||||||
|
aac_info2 = AacMediaCodecInformation(
|
||||||
|
AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
|
||||||
|
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
|
||||||
|
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
|
||||||
|
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE,
|
||||||
|
AacMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
|
| AacMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||||
|
AacMediaCodecInformation.Channels.MONO
|
||||||
|
| AacMediaCodecInformation.Channels.STEREO,
|
||||||
|
1,
|
||||||
|
256000,
|
||||||
|
)
|
||||||
|
assert aac_info == aac_info2
|
||||||
|
assert bytes(aac_info2) == bytes.fromhex("f0018c83e800")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_opus_codec_specific_information():
|
||||||
|
opus_info = OpusMediaCodecInformation.from_bytes(bytes([0x92]))
|
||||||
|
assert opus_info.vendor_id == OpusMediaCodecInformation.VENDOR_ID
|
||||||
|
assert opus_info.codec_id == OpusMediaCodecInformation.CODEC_ID
|
||||||
|
assert opus_info.frame_size == OpusMediaCodecInformation.FrameSize.FS_20MS
|
||||||
|
assert opus_info.channel_mode == OpusMediaCodecInformation.ChannelMode.STEREO
|
||||||
|
assert (
|
||||||
|
opus_info.sampling_frequency
|
||||||
|
== OpusMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
|
)
|
||||||
|
|
||||||
|
opus_info2 = OpusMediaCodecInformation(
|
||||||
|
OpusMediaCodecInformation.ChannelMode.STEREO,
|
||||||
|
OpusMediaCodecInformation.FrameSize.FS_20MS,
|
||||||
|
OpusMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||||
|
)
|
||||||
|
assert opus_info2 == opus_info
|
||||||
|
assert opus_info2.value == bytes([0x92])
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def async_main():
|
||||||
|
test_sbc_codec_specific_information()
|
||||||
|
test_aac_codec_specific_information()
|
||||||
|
test_opus_codec_specific_information()
|
||||||
await test_self_connection()
|
await test_self_connection()
|
||||||
await test_source_sink_1()
|
await test_source_sink_1()
|
||||||
|
|
||||||
@@ -281,4 +398,4 @@ async def run_test_self():
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||||
asyncio.run(run_test_self())
|
asyncio.run(async_main())
|
||||||
|
|||||||
@@ -23,13 +23,12 @@ from bumble.avdtp import (
|
|||||||
AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY,
|
AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY,
|
||||||
AVDTP_SET_CONFIGURATION,
|
AVDTP_SET_CONFIGURATION,
|
||||||
Message,
|
Message,
|
||||||
MediaPacket,
|
|
||||||
Get_Capabilities_Response,
|
Get_Capabilities_Response,
|
||||||
Set_Configuration_Command,
|
Set_Configuration_Command,
|
||||||
Set_Configuration_Response,
|
|
||||||
ServiceCapabilities,
|
ServiceCapabilities,
|
||||||
MediaCodecCapabilities,
|
MediaCodecCapabilities,
|
||||||
)
|
)
|
||||||
|
from bumble.rtp import MediaPacket
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
import random
|
||||||
import pytest
|
import pytest
|
||||||
from bumble.codecs import AacAudioRtpPacket, BitReader
|
from bumble.codecs import AacAudioRtpPacket, BitReader, BitWriter
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -49,19 +50,58 @@ def test_reader():
|
|||||||
assert value == int.from_bytes(data, byteorder='big')
|
assert value == int.from_bytes(data, byteorder='big')
|
||||||
|
|
||||||
|
|
||||||
|
def test_writer():
|
||||||
|
writer = BitWriter()
|
||||||
|
assert bytes(writer) == b''
|
||||||
|
|
||||||
|
for i in range(100):
|
||||||
|
for j in range(1, 10):
|
||||||
|
writer = BitWriter()
|
||||||
|
chunks = []
|
||||||
|
for k in range(j):
|
||||||
|
n_bits = random.randint(1, 32)
|
||||||
|
random_bits = random.getrandbits(n_bits)
|
||||||
|
chunks.append((n_bits, random_bits))
|
||||||
|
writer.write(random_bits, n_bits)
|
||||||
|
|
||||||
|
written_data = bytes(writer)
|
||||||
|
reader = BitReader(written_data)
|
||||||
|
for n_bits, written_bits in chunks:
|
||||||
|
read_bits = reader.read(n_bits)
|
||||||
|
assert read_bits == written_bits
|
||||||
|
|
||||||
|
|
||||||
def test_aac_rtp():
|
def test_aac_rtp():
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
packet_data = bytes.fromhex(
|
packet_data = bytes.fromhex(
|
||||||
'47fc0000b090800300202066000198000de120000000000000000000000000000000000000000000001c'
|
'47fc0000b090800300202066000198000de120000000000000000000000000000000000000000000001c'
|
||||||
)
|
)
|
||||||
packet = AacAudioRtpPacket(packet_data)
|
packet = AacAudioRtpPacket.from_bytes(packet_data)
|
||||||
adts = packet.to_adts()
|
adts = packet.to_adts()
|
||||||
assert adts == bytes.fromhex(
|
assert adts == bytes.fromhex(
|
||||||
'fff1508004fffc2066000198000de120000000000000000000000000000000000000000000001c'
|
'fff1508004fffc2066000198000de120000000000000000000000000000000000000000000001c'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
payload = bytes(list(range(1, 200)))
|
||||||
|
rtp = AacAudioRtpPacket.for_simple_aac(44100, 2, payload)
|
||||||
|
assert rtp.audio_mux_element.payload == payload
|
||||||
|
assert (
|
||||||
|
rtp.audio_mux_element.stream_mux_config.audio_specific_config.sampling_frequency
|
||||||
|
== 44100
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
rtp.audio_mux_element.stream_mux_config.audio_specific_config.channel_configuration
|
||||||
|
== 2
|
||||||
|
)
|
||||||
|
rtp2 = AacAudioRtpPacket.from_bytes(bytes(rtp))
|
||||||
|
assert str(rtp2.audio_mux_element.stream_mux_config) == str(
|
||||||
|
rtp.audio_mux_element.stream_mux_config
|
||||||
|
)
|
||||||
|
assert rtp2.audio_mux_element.payload == rtp.audio_mux_element.payload
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
test_reader()
|
test_reader()
|
||||||
|
test_writer()
|
||||||
test_aac_rtp()
|
test_aac_rtp()
|
||||||
|
|||||||
@@ -28,26 +28,18 @@ from bumble.avdtp import (
|
|||||||
AVDTP_AUDIO_MEDIA_TYPE,
|
AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
Listener,
|
Listener,
|
||||||
MediaCodecCapabilities,
|
MediaCodecCapabilities,
|
||||||
MediaPacket,
|
|
||||||
Protocol,
|
Protocol,
|
||||||
)
|
)
|
||||||
from bumble.a2dp import (
|
from bumble.a2dp import (
|
||||||
make_audio_sink_service_sdp_records,
|
make_audio_sink_service_sdp_records,
|
||||||
MPEG_2_AAC_LC_OBJECT_TYPE,
|
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
SBC_MONO_CHANNEL_MODE,
|
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
|
||||||
SbcMediaCodecInformation,
|
SbcMediaCodecInformation,
|
||||||
AacMediaCodecInformation,
|
AacMediaCodecInformation,
|
||||||
)
|
)
|
||||||
from bumble.utils import AsyncRunner
|
|
||||||
from bumble.codecs import AacAudioRtpPacket
|
from bumble.codecs import AacAudioRtpPacket
|
||||||
from bumble.hci import HCI_Reset_Command
|
from bumble.hci import HCI_Reset_Command
|
||||||
|
from bumble.rtp import MediaPacket
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -72,7 +64,7 @@ class AudioExtractor:
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AacAudioExtractor:
|
class AacAudioExtractor:
|
||||||
def extract_audio(self, packet: MediaPacket) -> bytes:
|
def extract_audio(self, packet: MediaPacket) -> bytes:
|
||||||
return AacAudioRtpPacket(packet.payload).to_adts()
|
return AacAudioRtpPacket.from_bytes(packet.payload).to_adts()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -130,10 +122,12 @@ class Speaker:
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
media_codec_information=AacMediaCodecInformation.from_lists(
|
media_codec_information=AacMediaCodecInformation(
|
||||||
object_types=[MPEG_2_AAC_LC_OBJECT_TYPE],
|
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
|
||||||
sampling_frequencies=[48000, 44100],
|
sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channels=[1, 2],
|
| AacMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||||
|
channels=AacMediaCodecInformation.Channels.MONO
|
||||||
|
| AacMediaCodecInformation.Channels.STEREO,
|
||||||
vbr=1,
|
vbr=1,
|
||||||
bitrate=256000,
|
bitrate=256000,
|
||||||
),
|
),
|
||||||
@@ -143,20 +137,23 @@ class Speaker:
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channel_modes=[
|
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
SBC_MONO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
],
|
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
block_lengths=[4, 8, 12, 16],
|
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
subbands=[4, 8],
|
block_length=SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
allocation_methods=[
|
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_16,
|
||||||
],
|
subbands=SbcMediaCodecInformation.Subbands.S_4
|
||||||
|
| SbcMediaCodecInformation.Subbands.S_8,
|
||||||
|
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||||
|
| SbcMediaCodecInformation.AllocationMethod.SNR,
|
||||||
minimum_bitpool_value=2,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
@@ -282,9 +279,6 @@ class Speaker:
|
|||||||
mitm=False
|
mitm=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start the controller
|
|
||||||
await self.device.power_on()
|
|
||||||
|
|
||||||
# Listen for Bluetooth connections
|
# Listen for Bluetooth connections
|
||||||
self.device.on('connection', self.on_bluetooth_connection)
|
self.device.on('connection', self.on_bluetooth_connection)
|
||||||
|
|
||||||
@@ -295,6 +289,9 @@ class Speaker:
|
|||||||
self.avdtp_listener = Listener.for_device(self.device)
|
self.avdtp_listener = Listener.for_device(self.device)
|
||||||
self.avdtp_listener.on('connection', self.on_avdtp_connection)
|
self.avdtp_listener.on('connection', self.on_avdtp_connection)
|
||||||
|
|
||||||
|
# Start the controller
|
||||||
|
await self.device.power_on()
|
||||||
|
|
||||||
print(f'Speaker ready to play, codec={self.codec}')
|
print(f'Speaker ready to play, codec={self.codec}')
|
||||||
|
|
||||||
if connect_address:
|
if connect_address:
|
||||||
|
|||||||
Reference in New Issue
Block a user