[pulseaudio-discuss] [PATCH v7 04/13] bluetooth: Modular API for A2DP codecs

Pali Rohár pali.rohar at gmail.com
Fri Apr 5 10:36:05 UTC 2019


On Thursday 04 April 2019 18:19:32 Tanu Kaskinen wrote:
> On Sat, 2019-02-23 at 10:45 +0100, Pali Rohár wrote:
> > +typedef struct pa_a2dp_codec {
> > +    /* Unique name of the codec, lowercase and without whitespaces, used for constructing identifier, D-Bus paths, ... */
> > +    const char *codec_name;
> > +    /* Human readable codec description */
> > +    const char *codec_description;
> > +
> > +    /* A2DP codec id */
> > +    pa_a2dp_codec_id codec_id;
> 
> I agree with Luiz that it would be good to drop "codec_" from the names
> of these variables.

Ok, I will remove them.

> > +
> > +    /* True if codec is bi-directional and supports backchannel */
> > +    bool support_backchannel;
> > +
> > +    /* Returns true if codec accepts capabilities, for_encoding is true when capabilities are used for encoding */
> > +    bool (*accept_capabilities)(const uint8_t *capabilities_buffer, uint8_t capabilities_size, bool for_encoding);
> > +    /* Choose preferred capabilities from hash map (const char * -> const pa_a2dp_codec_capabilities *) and returns corresponding key, for encoder is true when capabilities hash map is used for encoding */
> 
> Typo: "for encoder" should be "for_encoder".

Ok.

> Also, comments should be wrapped at 80 characters, that makes them
> easier to read.

Ok.

> > +    const char *(*choose_capabilities)(const pa_hashmap *capabilities_hashmap, const pa_sample_spec *default_sample_spec, bool for_encoding);
> 
> Returning a string key rather than a pa_a2dp_codec_capabilities looked
> weird, and the comment didn't explain what the hashmap keys should look
> like. I looked up how choose_capabilities() is used, and it looks like
> the returned key is an endpoint path. I think choose_remote_endpoint
> would be a better name for this callback, and the documentation should
> explain that the keys are remote endpoint paths.

Yes, it returns name of remote endpoint based on capabilities_hashmap
(capability --> endpoint).

choose_remote_endpoint seems like a better name, I will change it and
also update documentation.

> It seems that returning NULL is allowed when none of the endpoints can
> be accepted. It would be good to document that.

Yes.

> > +    /* Fill codec capabilities, returns size of filled buffer */
> > +    uint8_t (*fill_capabilities)(uint8_t capabilities_buffer[254]);
> 
> I think we should use a define for the maximum capabilities buffer
> size.

254 is limit by A2DP protocol itself. I can define macro for it.

> > +    /* Validate codec configuration, returns true on success */
> > +    bool (*validate_configuration)(const uint8_t *config_buffer, uint8_t config_size);
> 
> I commented earlier:
> 
>     The bool return value is used for reporting failures, but according
>     to our coding style failures should be reported using a negative
>     int.
> 
> You replied:
> 
>     It is really needed to use integer return value for true/false?
> 
> I never answered that question (sorry). When the result is about
> success/failure, then an int should be used. If the result is a yes/no
> answer, then bool is correct. If the function name was
> is_configuration_valid, then you could use bool here.

Because this codec API is slightly complicated, due to A2DP, I would
rather use C types which describe returned type unambiguously. So for
boolean value is C bool better. So if name "is_configuration_valid" is
acceptable with return type "bool" I will rename this function.

> > +    /* Fill preferred codec configuration, returns size of filled buffer */
> > +    uint8_t (*fill_preferred_configuration)(const pa_sample_spec *default_sample_spec, const uint8_t *capabilities_buffer, uint8_t capabilities_size, uint8_t config_buffer[254]);
> 
> It should be documented that the callback should return 0 on failure.

Ok.

> > +
> > +    /* Initialize codec, returns codec info data and set sample_spec, for_encoding is true when codec_info is used for encoding, for_backchannel is true when codec_info is used for backchannel */
> > +    void *(*init_codec)(bool for_encoding, bool for_backchannel, const uint8_t *config_buffer, uint8_t config_size, pa_sample_spec *sample_spec);
> 
> In order to be more consistent with other abstraction structs, I'd like
> to add a void pointer variable called "userdata" to pa_a2dp_codec.

This is not possible. Instances of pa_a2dp_codec struct are statically
defined directly in implementation of codec and are constant. So you
cannot put there some dynamic data, like user pointers. See e.g.
pa_a2dp_codec_aptx_hd how it is used.

> init_codec() would set that variable instead of returning the data. All
> callbacks would receive a pa_a2dp_codec pointer as a parameter instead
> of a void pointer.
> 
> > +    /* Deinitialize and release codec info data in codec_info */
> > +    void (*finish_codec)(void *codec_info);
> > +    /* Reset internal state of codec info data in codec_info */
> > +    void (*reset_codec)(void *codec_info);
> > +
> > +    /* Get read block size for codec */
> > +    size_t (*get_read_block_size)(void *codec_info, size_t read_link_mtu);
> > +    /* Get write block size for codec */
> > +    size_t (*get_write_block_size)(void *codec_info, size_t write_link_mtu);
> > +
> > +    /* Reduce encoder bitrate for codec, returns new write block size or zero if not changed, called when socket is not accepting encoded data fast enough */
> > +    size_t (*reduce_encoder_bitrate)(void *codec_info, size_t write_link_mtu);
> > +
> > +    /* Encode input_buffer of input_size to output_buffer of output_size, returns size of filled ouput_buffer and set processed to size of processed input_buffer */
> > +    size_t (*encode_buffer)(void *codec_info, uint32_t timestamp, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size, size_t *processed);
> > +    /* Decode input_buffer of input_size to output_buffer of output_size, returns size of filled ouput_buffer and set processed to size of processed input_buffer */
> > +    size_t (*decode_buffer)(void *codec_info, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size, size_t *processed);
> > +} pa_a2dp_codec;
> > +
> > +#endif

> > diff --git a/src/modules/bluetooth/a2dp-codec-sbc.c b/src/modules/bluetooth/a2dp-codec-sbc.c
> > new file mode 100644
> > index 000000000..3db827db3
> > --- /dev/null
> > +++ b/src/modules/bluetooth/a2dp-codec-sbc.c
> > +    if (!(capabilities->subbands & (SBC_SUBBANDS_4 | SBC_SUBBANDS_8)))
> > +        return false;
> > +
> > +    if (!(capabilities->block_length & (SBC_BLOCK_LENGTH_4 | SBC_BLOCK_LENGTH_8 | SBC_BLOCK_LENGTH_12 | SBC_BLOCK_LENGTH_16)))
> > +        return false;
> > +
> > +    return true;
> > +}
> > +
> > +static const char *choose_capabilities(const pa_hashmap *capabilities_hashmap, bool for_encoding) {
> 
> The default_sample_spec argument is missing.

Ou, that is bug. I will fix it.

> > +    config->min_bitpool = (uint8_t) PA_MAX(SBC_MIN_BITPOOL, capabilities->min_bitpool);
> > +    config->max_bitpool = (uint8_t) PA_MIN(default_bitpool(config->frequency, config->channel_mode), capabilities->max_bitpool);
> > +
> > +    if (config->min_bitpool > config->max_bitpool)
> > +        return 0;
> 
> Logging an error message would be helpful here.

This is just code movement. And your comment is fixed in patch 05/13.

> > +
> > +    return sizeof(*config);
> > +}

> > diff --git a/src/modules/bluetooth/a2dp-codec-util.c b/src/modules/bluetooth/a2dp-codec-util.c
> > new file mode 100644
> > index 000000000..27128d8ae
> > --- /dev/null
> > +++ b/src/modules/bluetooth/a2dp-codec-util.c
> > +const pa_a2dp_codec *pa_bluetooth_a2dp_codec(const char *name) {
> 
> Some bikeshedding: I'd prefer pa_bluetooth_get_a2dp_codec as the name.

Ok.

> > diff --git a/src/modules/bluetooth/bluez5-util.c b/src/modules/bluetooth/bluez5-util.c
> > index 485a57515..e4450cfe3 100644
> > --- a/src/modules/bluetooth/bluez5-util.c
> > +++ b/src/modules/bluetooth/bluez5-util.c
> > @@ -950,6 +943,7 @@ static void parse_interfaces_and_properties(pa_bluetooth_discovery *y, DBusMessa
> >  
> >          if (pa_streq(interface, BLUEZ_ADAPTER_INTERFACE)) {
> >              pa_bluetooth_adapter *a;
> > +            unsigned a2dp_codec_i, a2dp_codec_count;
> 
> The a2dp_codec_count variable doesn't have much value, since it's used
> only once and can be replaced with the pa_bluetooth_a2dp_codec_count()
> call.

This is prevention to be called this external function N times. It is
used as condition in for() loop and if I replace it on that place like
you wrote it would be called every iteration of loop. Function is cannot
be inlined as it is defined in .c file.

> > +            /* Order is important. bluez prefers endpoints registered earlier.
> > +             * And codec with higher number has higher priority. So iterate in reverse order. */
> > +            a2dp_codec_count = pa_bluetooth_a2dp_codec_count();
> > +            for (a2dp_codec_i = a2dp_codec_count; a2dp_codec_i > 0; a2dp_codec_i--) {
> > +                const pa_a2dp_codec *a2dp_codec = pa_bluetooth_a2dp_codec_iter(a2dp_codec_i-1);
> > +                char *endpoint;
> > +
> > +                endpoint = pa_sprintf_malloc("%s/%s", A2DP_SINK_ENDPOINT, a2dp_codec->codec_name);
> > +                register_endpoint(y, a2dp_codec, path, endpoint, PA_BLUETOOTH_UUID_A2DP_SINK);
> > +                pa_xfree(endpoint);
> > +
> > +                endpoint = pa_sprintf_malloc("%s/%s", A2DP_SOURCE_ENDPOINT, a2dp_codec->codec_name);
> > +                register_endpoint(y, a2dp_codec, path, endpoint, PA_BLUETOOTH_UUID_A2DP_SOURCE);
> > +                pa_xfree(endpoint);
> > +            }

> > diff --git a/src/modules/bluetooth/module-bluez5-device.c b/src/modules/bluetooth/module-bluez5-device.c
> > index 3e72ac3c1..531bc25a5 100644
> > --- a/src/modules/bluetooth/module-bluez5-device.c
> > +++ b/src/modules/bluetooth/module-bluez5-device.c
> > @@ -413,115 +409,41 @@ static int sco_process_push(struct userdata *u) {
> >  }
> >  
> >  /* Run from IO thread */
> > -static void a2dp_prepare_buffer(struct userdata *u) {
> > +static void a2dp_prepare_encoder_buffer(struct userdata *u) {
> >      size_t min_buffer_size = PA_MAX(u->read_link_mtu, u->write_link_mtu);
> 
> Since you added separate "prepare buffer" functions for encoding and
> decoding, it seems that the PA_MAX() usage is unnecessary, and you
> should just use u->write_link_mtu here (and u->read_link_mtu in
> prepare_decoder_buffer).

Good catch!

> > @@ -620,50 +569,18 @@ static int a2dp_process_push(struct userdata *u) {
> >              tstamp = pa_rtclock_now();
> >          }
> >  
> > -        p = (uint8_t*) sbc_info->buffer + sizeof(*header) + sizeof(*payload);
> > -        to_decode = l - sizeof(*header) - sizeof(*payload);
> > -
> > -        d = pa_memblock_acquire(memchunk.memblock);
> > -        to_write = memchunk.length = pa_memblock_get_length(memchunk.memblock);
> > -
> > -        while (PA_LIKELY(to_decode > 0)) {
> > -            size_t written;
> > -            ssize_t decoded;
> > +        ptr = pa_memblock_acquire(memchunk.memblock);
> > +        memchunk.length = pa_memblock_get_length(memchunk.memblock);
> >  
> > -            decoded = sbc_decode(&sbc_info->sbc,
> > -                                 p, to_decode,
> > -                                 d, to_write,
> > -                                 &written);
> > -
> > -            if (PA_UNLIKELY(decoded <= 0)) {
> > -                pa_log_error("SBC decoding error (%li)", (long) decoded);
> > -                pa_memblock_release(memchunk.memblock);
> > -                pa_memblock_unref(memchunk.memblock);
> > -                return 0;
> > -            }
> > -
> > -            total_written += written;
> > -
> > -            /* Reset frame length, it can be changed due to bitpool change */
> > -            sbc_info->frame_length = sbc_get_frame_length(&sbc_info->sbc);
> > -
> > -            pa_assert_fp((size_t) decoded <= to_decode);
> > -            pa_assert_fp((size_t) decoded == sbc_info->frame_length);
> > -
> > -            pa_assert_fp((size_t) written == sbc_info->codesize);
> > -
> > -            p = (const uint8_t*) p + decoded;
> > -            to_decode -= decoded;
> > -
> > -            d = (uint8_t*) d + written;
> > -            to_write -= written;
> > -        }
> > +        total_written = u->a2dp_codec->decode_buffer(u->decoder_info, u->decoder_buffer, l, ptr, memchunk.length, &processed);
> > +        if (total_written == 0)
> > +            return 0;
> >  
> >          u->read_index += (uint64_t) total_written;
> > -        pa_smoother_put(u->read_smoother, tstamp, pa_bytes_to_usec(u->read_index, &u->sample_spec));
> > +        pa_smoother_put(u->read_smoother, tstamp, pa_bytes_to_usec(u->read_index, &u->decoder_sample_spec));
> >          pa_smoother_resume(u->read_smoother, tstamp, true);
> >  
> > -        memchunk.length -= to_write;
> > +        memchunk.length -= l - processed;
> 
> This doesn't make sense to me, but maybe I'm missing something, since
> you've probably tested that this works fine... memchunk.length should
> be the length of uncompressed audio that was decoded, but
> memchunk.length is initially set to read_block_size, i.e. the maximum
> possible amount of compressed data that can be read in one go, and then
> "l - processed" is subtracted, and "l - processed" is the amount of
> compressed audio that was read but for some reason not processed. So
> memchunk.length is set to the maximum amount of compressed data minus
> received but unprocessed compressed data. That doesn't seem to have
> anything to do with the length of uncompressed audio that was decoded.

to_write was passed as argument to sbc_decode function which describes
length of output buffer for decoded data.

Until there are some data to decode, to_write is decremented by length
of written decoded data. So to_write hold length of buffer need for
decoded data every step.

So you are right that (l-processed) is *not* same as to_write. And this
is wrong.

> Why is memchunk.length not simply set to the return value of
> decode_buffer()?

I looked at it again and seems that you are right. Return value of
decode_buffer() gives us length of data written into output buffer which
is really what subtraction by to_write from memchunk.length returns.

I will change it.

> >  
> >          pa_memblock_release(memchunk.memblock);
> >  

> > @@ -865,28 +734,22 @@ static void transport_config_mtu(struct userdata *u) {
> >              u->write_block_size = pa_frame_align(u->write_block_size, &u->sink->sample_spec);
> >          }
> >      } else {
> > -        u->read_block_size =
> > -            (u->read_link_mtu - sizeof(struct rtp_header) - sizeof(struct rtp_payload))
> > -            / u->sbc_info.frame_length * u->sbc_info.codesize;
> > -
> > -        u->write_block_size =
> > -            (u->write_link_mtu - sizeof(struct rtp_header) - sizeof(struct rtp_payload))
> > -            / u->sbc_info.frame_length * u->sbc_info.codesize;
> > +        pa_assert(u->a2dp_codec);
> > +        if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) {
> > +            u->write_block_size = u->a2dp_codec->get_write_block_size(u->encoder_info, u->write_link_mtu);
> > +        } else {
> > +            u->read_block_size = u->a2dp_codec->get_read_block_size(u->encoder_info, u->read_link_mtu);
> 
> Should be decoder_info instead of encoder_info.

Yes! This is bug.

> > @@ -911,11 +782,6 @@ static void setup_stream(struct userdata *u) {
> >  
> >      pa_log_debug("Stream properly set up, we're ready to roll!");
> >  
> > -    if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) {
> > -        a2dp_set_bitpool(u, u->sbc_info.max_bitpool);
> 
> It looks like resetting the bitpool isn't done anywhere. Should it be
> done in the reset_codec() callback?

reset_codec() for SBC calls set_params() and it resets sbc.bitpool to
default value. So reset_codec() calls resets bitpool.

> > -        update_buffer_size(u);
> > -    }
> > -
> >      u->rtpoll_item = pa_rtpoll_item_new(u->rtpoll, PA_RTPOLL_NEVER, 1);
> >      pollfd = pa_rtpoll_item_get_pollfd(u->rtpoll_item, NULL);
> >      pollfd->fd = u->stream_fd;

> > @@ -2497,11 +2301,18 @@ void pa__done(pa_module *m) {
> >      if (u->transport_microphone_gain_changed_slot)
> >          pa_hook_slot_free(u->transport_microphone_gain_changed_slot);
> >  
> > -    if (u->sbc_info.buffer)
> > -        pa_xfree(u->sbc_info.buffer);
> > +    if (u->encoder_buffer)
> > +        pa_xfree(u->encoder_buffer);
> >  
> > -    if (u->sbc_info.sbc_initialized)
> > -        sbc_finish(&u->sbc_info.sbc);
> > +    if (u->decoder_buffer)
> > +        pa_xfree(u->decoder_buffer);
> > +
> > +    if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK || u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) {
> > +        if (u->encoder_info)
> > +            u->a2dp_codec->finish_codec(u->encoder_info);
> > +        if (u->decoder_info)
> > +            u->a2dp_codec->finish_codec(u->decoder_info);
> 
> stop_thread() ensures that encoder_info and decoder_info are NULL, so
> this code is unnecessary. You can add assertions if you like.

Ok.

-- 
Pali Rohár
pali.rohar at gmail.com


More information about the pulseaudio-discuss mailing list