Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

a2dp player #571

Merged
merged 6 commits into from
Oct 19, 2024
Merged

a2dp player #571

merged 6 commits into from
Oct 19, 2024

Conversation

barbibulle
Copy link
Collaborator

Simple CLI app to play audio to a speaker or headphones with A2DP.
This includes support for playing SBC, AAC and Ogg/Opus files.

bumble/a2dp.py Outdated
Comment on lines 140 to 142
OPUS_VENDOR_ID = 0x000000E0
OPUS_CODEC_ID = 0x0001
OPUS_MAX_FRAMES_IN_RTP_PAYLOAD = 15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these could be a part of OpusMediaCodecInformation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

bumble/a2dp.py Outdated
Comment on lines 549 to 558
class ChannelMode(enum.IntEnum):
MONO = 0
STEREO = 1
DUAL_MONO = 2

CHANNEL_MODE_BITS = {
ChannelMode.MONO: 1 << 0,
ChannelMode.STEREO: 1 << 1,
ChannelMode.DUAL_MONO: 1 << 2,
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these constants should be flag instead of enum, so that we don't need additional maps and from_discrete_values to build and serialize codec informations (also applicable to SBC and AAC).

class OpusCodecInformation:
  """OPUS codec information."""

  class ChannelMode(enum.IntFlag):
    MONO = 0x01
    STEREO = 0x02
    DUAL_MONO = 0x04


  class FrameSize(enum.IntFlag):
    SIZE_10_MILLISECONDS = 0x08
    SIZE_20_MILLISECONDS = 0x10


  class SamplingRate(enum.IntFlag):
    RATE_48000 = 0x80

  sample_rate: SamplingRate
  channel_mode: ChannelMode
  frame_size: FrameSize

  VENDOR_ID: ClassVar[int] = 0xE0
  CODEC_ID: ClassVar[int] = 0x01

  def __bytes__(self) -> bytes:
    return struct.pack(
        '<IHB',
        self.VENDOR_ID,
        self.CODEC_ID,
        self.sample_rate | self.channel_mode | self.frame_size,
    )

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that's much better. I've done that for all 3 types now.
Initially, I thought it would be convenient to treat some fields like sampling frequencies as ints instead of enums, but that just complicates everything for no good reason. Using IntFlag for everything is the right approach.

bumble/a2dp.py Outdated
Comment on lines 1064 to 1065
# pylint: disable=import-outside-toplevel
from .avdtp import MediaPacket # Import here to avoid a circular reference
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move MediaPacket to a third module so that we can avoid inline imports?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. MediaPacket is not in rtp.py

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is left

Comment on lines +1432 to +1444
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: If there isn't additional works here

Suggested change
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
has_codec = (isinstance(
codec_capabilities.media_codec_information,
VendorSpecificMediaCodecInformation,
) and codec_capabilities.media_codec_information.vendor_id == vendor_id
and codec_capabilities.media_codec_information.codec_id == codec_id)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not 100% the same: if we encounter a capabilities for a standard (non-vendor) codec, we want has_codec to be True.

bumble/a2dp.py Outdated
Comment on lines 984 to 988
(granule_position,) = struct.unpack_from("<Q", header, 6)
(bitstream_serial_number,) = struct.unpack_from("<I", header, 14)
(page_sequence_number,) = struct.unpack_from("<I", header, 18)
(crc_checksum,) = struct.unpack_from("<I", header, 22)
page_segments = header[26]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(granule_position,) = struct.unpack_from("<Q", header, 6)
(bitstream_serial_number,) = struct.unpack_from("<I", header, 14)
(page_sequence_number,) = struct.unpack_from("<I", header, 18)
(crc_checksum,) = struct.unpack_from("<I", header, 22)
page_segments = header[26]
(granule_position, bitstream_serial_number, page_sequence_number, crc_checksum, page_segments) = struct.unpack_from("<QIIIB", header, 6)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -51,6 +51,7 @@
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
A2DP_NON_A2DP_CODEC_TYPE,
A2DP_SBC_CODEC_TYPE,
A2DP_VENDOR_MEDIA_CODEC_INFORMATION_CLASSES,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Just a comment)

From a spec perspective, importing A2DP from AVDTP seems like a reversed dependencies, but top-down parsing is important for usage. I am thinking whether it's possible to hook parsing table from A2DP, but that will require callers to always import A2DP, or we will need to wrap AVDTP and A2DP stubs inside a public module which is too aggresive for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it would be nice to have some better way to have a sort of dependency injection here. I haven't found a great way to do that that's not intrusive. I'll try to think of some better way.

bumble/a2dp.py Outdated
Comment on lines 1064 to 1065
# pylint: disable=import-outside-toplevel
from .avdtp import MediaPacket # Import here to avoid a circular reference
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is left

@barbibulle barbibulle merged commit 1de7d2c into main Oct 19, 2024
57 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants